Feature - Ticket Summary: Possibility to add generated next steps to checklists
Co-authored-by: Benjamin Scharf <bs@zammad.com> Co-authored-by: Dusan Vuckovic <dv@zammad.com> Co-authored-by: Martin Gruner <mg@zammad.com>
This commit is contained in:
@@ -883,5 +883,10 @@
|
||||
"author": "Felix Niklas",
|
||||
"url": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
},
|
||||
"check2-square.svg": {
|
||||
"author": "Zammad",
|
||||
"url": "",
|
||||
"license": "MIT"
|
||||
},
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ class SidebarChecklist extends App.Controller
|
||||
done: =>
|
||||
@widget = new App.SidebarChecklistStart(el: @elSidebar, parentVC: @)
|
||||
)
|
||||
@ticket.checklist_id = undefined
|
||||
)
|
||||
|
||||
renderActions: =>
|
||||
|
||||
@@ -3,11 +3,12 @@ class SidebarTicketSummary extends App.Controller
|
||||
{ key: 'problem', name: __('Customer Intent'), value: 'problem' },
|
||||
{ key: 'conversation_summary', name: __('Conversation Summary'), value: 'conversation_summary' },
|
||||
{ key: 'open_questions', name: __('Open Questions'), value: 'open_questions', type: 'list' },
|
||||
{ key: 'suggestions', name: __('Suggested Next Steps'), value: 'suggestions', type: 'list' },
|
||||
{ key: 'suggestions', name: __('Suggested Next Steps'), value: 'suggestions', feature: 'checklist', type: 'list' },
|
||||
]
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
|
||||
@controllerBind('ui::ticket::summaryUpdate', @summaryLoaded)
|
||||
@controllerBind('config_update', @configUpdate)
|
||||
|
||||
@@ -15,7 +16,6 @@ class SidebarTicketSummary extends App.Controller
|
||||
if config.name is 'ai_assistance_ticket_summary_config'
|
||||
@parent.triggerArticleSummaryUpdate()
|
||||
|
||||
|
||||
summaryLoaded: (payload) =>
|
||||
return if !@elSidebar
|
||||
return if payload.ticket_id isnt @ticket.id
|
||||
@@ -25,6 +25,9 @@ class SidebarTicketSummary extends App.Controller
|
||||
getAvailableDisplayStructure: ->
|
||||
config = App.Config.get('ai_assistance_ticket_summary_config')
|
||||
|
||||
if not App.Config.get('checklist')
|
||||
@DISPLAY_STRUCTURE.find((item) -> item.key is 'suggestions').feature = undefined
|
||||
|
||||
@DISPLAY_STRUCTURE.filter((item) -> !(item.key of config) or config[item.key] is true)
|
||||
|
||||
sidebarItem: =>
|
||||
@@ -39,6 +42,7 @@ class SidebarTicketSummary extends App.Controller
|
||||
sidebarCallback: @sidebarCallback
|
||||
sidebarActions: []
|
||||
}
|
||||
|
||||
@item
|
||||
|
||||
shown: =>
|
||||
@@ -47,16 +51,114 @@ class SidebarTicketSummary extends App.Controller
|
||||
sidebarCallback: (el) =>
|
||||
@elSidebar = el
|
||||
|
||||
createChecklist: (callback) =>
|
||||
@ajax(
|
||||
id: 'checklist_ticket_create_empty'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/checklists"
|
||||
data: JSON.stringify({ ticket_id: @ticket.id })
|
||||
processData: true
|
||||
success: callback
|
||||
error: =>
|
||||
@notify(
|
||||
type: 'error'
|
||||
msg: App.i18n.translateInline('Unable to create checklist.')
|
||||
)
|
||||
)
|
||||
|
||||
convertToChecklistItem: (event) =>
|
||||
target = $(event.target).closest('.js-btn-add-checklist-item')
|
||||
return if !target
|
||||
|
||||
text = $(target).data('content')
|
||||
checklistId = @ticket.checklist_id
|
||||
|
||||
if !checklistId
|
||||
@createChecklist((data) =>
|
||||
@ticket.checklist_id = data.id
|
||||
@createChecklistItem(data.id, text)
|
||||
)
|
||||
else
|
||||
@createChecklistItem(checklistId, text)
|
||||
|
||||
createChecklistItem: (checklistId, text) =>
|
||||
item = new App.ChecklistItem
|
||||
item.checklist_id = checklistId
|
||||
item.text = text
|
||||
|
||||
item.save(
|
||||
done: =>
|
||||
@notify(
|
||||
type: 'success'
|
||||
msg: App.i18n.translateInline('Checklist item successfully added.')
|
||||
)
|
||||
fail: =>
|
||||
@notify(
|
||||
type: 'error'
|
||||
msg: App.i18n.translateInline('Unable to add checklist item.')
|
||||
)
|
||||
)
|
||||
|
||||
convertAllToChecklistItems: =>
|
||||
checklistId = @ticket.checklist_id
|
||||
|
||||
itemData = _.map(@summaryItems.suggestions, (text) ->
|
||||
text: text
|
||||
checked: false
|
||||
)
|
||||
|
||||
if !checklistId
|
||||
checklist = new App.Checklist
|
||||
checklist = @createChecklist((data) =>
|
||||
@ticket.checklist_id = data.id
|
||||
@createBulkChecklistItems(data.id, itemData)
|
||||
)
|
||||
else
|
||||
@createBulkChecklistItems(checklistId, itemData)
|
||||
|
||||
openChecklist: =>
|
||||
@elSidebar
|
||||
.closest('.content')
|
||||
.find(".tabsSidebar-tab[data-tab='checklist']:not(.active)")
|
||||
.click()
|
||||
|
||||
createBulkChecklistItems: (checklistId, items) =>
|
||||
@ajax(
|
||||
id: 'checklist_ticket_create_bulk'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/checklist_items/create_bulk"
|
||||
processData: true
|
||||
data: JSON.stringify(
|
||||
checklist_id: checklistId
|
||||
items: items
|
||||
)
|
||||
success: =>
|
||||
@openChecklist()
|
||||
error: =>
|
||||
@notify(
|
||||
type: 'error'
|
||||
msg: App.i18n.translateInline('Unable to add all checklist items.')
|
||||
)
|
||||
)
|
||||
|
||||
showSummarization: (payload) =>
|
||||
noSummaryPossible = payload?.data?.result && Object.values(payload.data.result).every((item) -> item == null)
|
||||
return if not payload?.data
|
||||
|
||||
@summaryItems = payload.data.result
|
||||
|
||||
noSummaryPossible = @summaryItems and _.every(_.values(@summaryItems), (item) -> item is null)
|
||||
|
||||
summarization = $(App.view('ticket_zoom/sidebar_ticket_summary')(
|
||||
data: payload?.data,
|
||||
data: payload.data,
|
||||
noSummaryPossible: noSummaryPossible
|
||||
structure: @getAvailableDisplayStructure()
|
||||
structure: @getAvailableDisplayStructure()
|
||||
))
|
||||
|
||||
@elSidebar.html(summarization)
|
||||
@elSidebar.find('.js-retry').on('click', @retrySummarization)
|
||||
|
||||
@elSidebar.find('.js-retry').off('click.retry').on('click.retry', @retrySummarization)
|
||||
@elSidebar.find('.js-btn-add-checklist-item').off('click.convert').on('click.convert', @convertToChecklistItem)
|
||||
@elSidebar.find('.js-btn-ticket-summary-add-all-to-checklist').off('click.convert_all').on('click.convert_all', @convertAllToChecklistItems)
|
||||
|
||||
retrySummarization: (e) =>
|
||||
@preventDefaultAndStopPropagation(e)
|
||||
|
||||
@@ -21,23 +21,48 @@
|
||||
<% else if @data?.result: %>
|
||||
<% for item in @structure: %>
|
||||
<% if !_.isEmpty(@data.result[item.value]): %>
|
||||
<h4><%- @T(item.name) %></h4>
|
||||
<% if item.type is 'list': %>
|
||||
<ul>
|
||||
<% for localList in @data.result[item.value]: %>
|
||||
<li><%= localList %></li>
|
||||
<div class="ticket-summary-sidebar-item">
|
||||
<h3><%- @T(item.name) %></h4>
|
||||
<% if item.type is 'list': %>
|
||||
<% if item?.feature is 'checklist': %>
|
||||
<ul class="js-ticket-summary-list">
|
||||
<% for localList in @data.result[item.value]: %>
|
||||
<li class="ticket-summary-sidebar-list-item-action">
|
||||
<span>
|
||||
<%= localList %>
|
||||
</span>
|
||||
<button class="btn btn--secondary btn--action btn--small js-btn-add-checklist-item" data-content="<%= localList %>" aria-label="<%- @T('Add as checklist item') %>" type="button">
|
||||
<%- @Icon 'checkmark' %>
|
||||
<span><%- @T('Add') %></span>
|
||||
</button>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else: %>
|
||||
<ul>
|
||||
<% for localList in @data.result[item.value]: %>
|
||||
<li><%= localList %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% else: %>
|
||||
<p><%= @data.result[item.value] %></p>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else: %>
|
||||
<p><%= @data.result[item.value] %><p/>
|
||||
<% end %>
|
||||
<% if item?.feature is 'checklist': %>
|
||||
<button class="btn btn--secondary btn--action btn--small js-btn-ticket-summary-add-all-to-checklist" type="button">
|
||||
<%- @Icon 'double-checkmark' %>
|
||||
<span><%- @T('Add all to checklist') %></span>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<span class="text-muted text-small">
|
||||
<%- @T('*Be sure to check AI-generated summaries for accuracy') %>
|
||||
</div>
|
||||
<p class="text-muted text-small">
|
||||
<%- @T('*Be sure to check AI-generated summaries for accuracy') %>
|
||||
</p>
|
||||
<% else: %>
|
||||
<p><%- @T('Zammad Smart Assist is generating the summary for you…') %><p/>
|
||||
<p><%- @T('Zammad Smart Assist is generating the summary for you…') %></p>
|
||||
<div class="loading-container">
|
||||
<div class="loading icon"></div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
.icon-bold { width: 12px; height: 12px; }
|
||||
.icon-chain { width: 16px; height: 16px; }
|
||||
.icon-chat { width: 24px; height: 24px; }
|
||||
.icon-check2-square { width: 16px; height: 16px; }
|
||||
.icon-checkbox-checked-readonly { width: 11px; height: 11px; }
|
||||
.icon-checkbox-checked { width: 11px; height: 11px; }
|
||||
.icon-checkbox-indeterminate { width: 11px; height: 11px; }
|
||||
|
||||
@@ -7260,8 +7260,11 @@ a.list-group-item.active > .badge,
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1 1 150px;
|
||||
|
||||
* {
|
||||
@include bidi-style(margin-right, 10px, margin-left, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
@@ -7272,25 +7275,103 @@ a.list-group-item.active > .badge,
|
||||
}
|
||||
|
||||
.ticket-ai-summary-buttons {
|
||||
max-width: fit-content;
|
||||
flex: 1 0 auto;
|
||||
flex: 0 0 auto;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
|
||||
.btn {
|
||||
flex: 0 0 fit-content;
|
||||
white-space: nowrap;
|
||||
margin-left: 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
@include bidi-style(margin-right, 10px, margin-left, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar[data-tab='summary'] {
|
||||
hr {
|
||||
height: 1px; /* +1px for the border */
|
||||
background: rgb(0, 158, 198);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 158, 198, 1) 0%,
|
||||
rgba(232, 79, 131, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.sidebar-block {
|
||||
margin-bottom: 0;
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ticket-summary-sidebar-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 16px;
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ticket-summary-sidebar-list-item-action {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.js-btn-add-checklist-item,
|
||||
.js-btn-ticket-summary-add-all-to-checklist {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.js-btn-add-checklist-item {
|
||||
margin-top: 4px;
|
||||
|
||||
.icon-checkmark {
|
||||
fill: var(--button-primary-background);
|
||||
}
|
||||
}
|
||||
|
||||
.js-btn-ticket-summary-add-all-to-checklist {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.ticket-summary-sidebar-item:has(
|
||||
.ticket-summary-sidebar-list-item-action
|
||||
)::before {
|
||||
content: '';
|
||||
margin-bottom: 16px;
|
||||
height: 1px;
|
||||
background: rgb(0, 158, 198);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 158, 198, 1) 0%,
|
||||
rgba(232, 79, 131, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.internal-border {
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
|
||||
@@ -11,6 +11,18 @@ class ChecklistItemsController < ApplicationController
|
||||
model_create_render(Checklist::Item, new_item_params)
|
||||
end
|
||||
|
||||
def create_bulk
|
||||
create_item_params = params.permit(:checklist_id, items: %i[text checked])
|
||||
|
||||
checklist = Checklist.find(params[:checklist_id])
|
||||
|
||||
created_items = create_item_params[:items].map do |item|
|
||||
checklist.items.create!(item)
|
||||
end
|
||||
|
||||
render json: { success: true, checklist_item_ids: created_items.map(&:id) }, status: :created
|
||||
end
|
||||
|
||||
def update
|
||||
model_update_render(Checklist::Item, existing_item_params)
|
||||
end
|
||||
|
||||
@@ -13,8 +13,10 @@ class ChecklistsController < ApplicationController
|
||||
checklist = if params[:template_id].present?
|
||||
template = ChecklistTemplate.find(params[:template_id])
|
||||
Checklist.create_from_template!(ticket, template)
|
||||
else
|
||||
elsif params[:create_first_item]
|
||||
Checklist.create_fresh!(ticket)
|
||||
else
|
||||
Checklist.create!(ticket:)
|
||||
end
|
||||
|
||||
render json: { id: checklist.id, assets: checklist.assets({}) }, status: :created
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 8C2.5 4.96243 4.96243 2.5 8 2.5C9.00262 2.5 9.94117 2.76782 10.7496 3.2355C10.9887 3.37377 11.2945 3.29209 11.4328 3.05306C11.5711 2.81403 11.4894 2.50816 11.2504 2.36989C10.2938 1.81654 9.1831 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 7.72386 14.2761 7.5 14 7.5C13.7239 7.5 13.5 7.72386 13.5 8C13.5 11.0376 11.0376 13.5 8 13.5C4.96243 13.5 2.5 11.0376 2.5 8Z" />
|
||||
<path d="M15.3536 3.35355C15.5488 3.15829 15.5488 2.84171 15.3536 2.64645C15.1583 2.45118 14.8417 2.45118 14.6464 2.64645L8 9.29289L5.35355 6.64645C5.15829 6.45118 4.84171 6.45118 4.64645 6.64645C4.45118 6.84171 4.45118 7.15829 4.64645 7.35355L7.64645 10.3536C7.84171 10.5488 8.15829 10.5488 8.35355 10.3536L15.3536 3.35355Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 866 B |
@@ -65,7 +65,8 @@ const createNewChecklist = async (
|
||||
return addNewChecklistMutation
|
||||
.send({
|
||||
...input,
|
||||
ticketId: ticket.value.id,
|
||||
createFirstItem: true,
|
||||
ticketId: ticket.value!.id,
|
||||
})
|
||||
.then(() => {
|
||||
if (options.focusLastItem)
|
||||
|
||||
@@ -343,6 +343,7 @@ describe('TicketSidebarChecklist', () => {
|
||||
const calls = await waitForTicketChecklistAddMutationCalls()
|
||||
|
||||
expect(calls.at(-1)?.variables).toEqual({
|
||||
createFirstItem: true,
|
||||
ticketId: convertToGraphQLId('Ticket', 1),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, useTemplateRef } from 'vue'
|
||||
import { onBeforeUnmount, onMounted, useTemplateRef, computed } from 'vue'
|
||||
|
||||
import type { ObjectLike } from '#shared/types/utils.ts'
|
||||
|
||||
@@ -14,9 +14,10 @@ interface Props {
|
||||
icon: string
|
||||
entity?: ObjectLike
|
||||
actions?: MenuItem[]
|
||||
variant?: 'ai'
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const scrollPosition = defineModel<number>({
|
||||
required: true,
|
||||
@@ -25,6 +26,17 @@ const scrollPosition = defineModel<number>({
|
||||
|
||||
const scrollContainer = useTemplateRef('scroll-container')
|
||||
|
||||
const variantClass = computed(() =>
|
||||
props.variant === 'ai'
|
||||
? 'ai-stripe before:-bottom-3 before:absolute relative before:left:0 before:right-0 before:h-0.5'
|
||||
: '',
|
||||
)
|
||||
|
||||
const iconClass = computed(() =>
|
||||
props.variant === 'ai'
|
||||
? 'text-blue-800'
|
||||
: 'text-stone-200 dark:text-neutral-500',
|
||||
)
|
||||
// Handle scroll position (re)storing of the active sidebar, when navigating between taskbar tabs.
|
||||
useScrollPosition(scrollContainer)
|
||||
|
||||
@@ -45,9 +57,10 @@ onBeforeUnmount(() => {
|
||||
<CommonLabel
|
||||
tag="h2"
|
||||
class="min-h-7 grow gap-1.5"
|
||||
:class="variantClass"
|
||||
size="large"
|
||||
:prefix-icon="icon"
|
||||
icon-color="text-stone-200 dark:text-neutral-500"
|
||||
:icon-color="iconClass"
|
||||
>
|
||||
{{ $t(title) }}
|
||||
</CommonLabel>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, effectScope, watch, type EffectScope, computed } from 'vue'
|
||||
|
||||
import { useTicketArticleUpdatesSubscription } from '#shared/entities/ticket/graphql/subscriptions/ticketArticlesUpdates.api.ts'
|
||||
@@ -17,9 +18,13 @@ import { useSessionStore } from '#shared/stores/session.ts'
|
||||
|
||||
import { useReactivate } from '#desktop/composables/useReactivate.ts'
|
||||
import TicketSidebarSummaryContent from '#desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue'
|
||||
import {
|
||||
type SummaryConfig,
|
||||
type SummaryItem,
|
||||
TicketSummaryFeature,
|
||||
} from '#desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/types.ts'
|
||||
import { usePersistentStates } from '#desktop/pages/ticket/composables/usePersistentStates.ts'
|
||||
import { useTicketInformation } from '#desktop/pages/ticket/composables/useTicketInformation.ts'
|
||||
import { useTicketSummary } from '#desktop/pages/ticket/composables/useTicketSummary.ts'
|
||||
import { useTicketAiAssistanceSummarizeMutation } from '#desktop/pages/ticket/graphql/mutations/ticketAIAssistanceSummarize.api.ts'
|
||||
import { useTicketAiAssistanceSummaryUpdatesSubscription } from '#desktop/pages/ticket/graphql/subscriptions/ticketAIAssistanceSummaryUpdates.api.ts'
|
||||
import type {
|
||||
@@ -32,7 +37,8 @@ import TicketSidebarWrapper from '../TicketSidebarWrapper.vue'
|
||||
defineProps<TicketSidebarProps>()
|
||||
|
||||
const { user, hasPermission } = useSessionStore()
|
||||
const { config } = useApplicationStore()
|
||||
|
||||
const { config } = storeToRefs(useApplicationStore())
|
||||
|
||||
const { persistentStates } = usePersistentStates()
|
||||
|
||||
@@ -40,7 +46,52 @@ const emit = defineEmits<TicketSidebarEmits>()
|
||||
|
||||
const { ticketId } = useTicketInformation()
|
||||
|
||||
const { isEnabled, isProviderConfigured } = useTicketSummary()
|
||||
const summaryConfig = computed(
|
||||
() => config.value.ai_assistance_ticket_summary_config as SummaryConfig,
|
||||
)
|
||||
|
||||
const isProviderConfigured = computed(() => !!config.value.ai_provider)
|
||||
|
||||
const { ticket } = useTicketInformation()
|
||||
|
||||
const isEnabled = computed(
|
||||
() =>
|
||||
!!(
|
||||
ticket.value &&
|
||||
ticket.value?.state.name !== 'merged' &&
|
||||
config.value.ai_assistance_ticket_summary
|
||||
),
|
||||
)
|
||||
|
||||
const headings = computed<SummaryItem[]>(() => [
|
||||
{
|
||||
key: 'problem',
|
||||
label: __('Customer Intent'),
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
key: 'conversationSummary',
|
||||
label: __('Conversation Summary'),
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
key: 'openQuestions',
|
||||
label: __('Open Questions'),
|
||||
active: summaryConfig.value.open_questions,
|
||||
},
|
||||
{
|
||||
key: 'suggestions',
|
||||
label: __('Suggested Next Steps'),
|
||||
active: summaryConfig.value.suggestions,
|
||||
feature: config.value.checklist
|
||||
? TicketSummaryFeature.Checklist
|
||||
: undefined,
|
||||
},
|
||||
])
|
||||
|
||||
const summaryHeadings = computed(() =>
|
||||
headings.value.filter((heading) => heading.active),
|
||||
)
|
||||
|
||||
const summary = ref<TicketAiAssistanceSummary | null>(null)
|
||||
const generationError = ref<AsyncExecutionError | null>(null)
|
||||
@@ -64,6 +115,12 @@ const getAIAssistanceSummary = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const retrySummaryGeneration = () => {
|
||||
summary.value = null
|
||||
generationError.value = null
|
||||
getAIAssistanceSummary()
|
||||
}
|
||||
|
||||
const activateSubscription = () => {
|
||||
const articleSubscription = new SubscriptionHandler(
|
||||
useTicketArticleUpdatesSubscription(
|
||||
@@ -90,7 +147,7 @@ const activateSubscription = () => {
|
||||
useTicketAiAssistanceSummaryUpdatesSubscription(
|
||||
{
|
||||
ticketId: ticketId.value,
|
||||
locale: user?.preferences?.locale || config.locale_default,
|
||||
locale: user?.preferences?.locale || config.value.locale_default,
|
||||
},
|
||||
() => ({
|
||||
enabled: isProviderConfigured.value,
|
||||
@@ -160,9 +217,11 @@ watch(
|
||||
:context="context"
|
||||
:sidebar-plugin="sidebarPlugin"
|
||||
:summary="summary"
|
||||
:summary-headings="summaryHeadings"
|
||||
:is-provider-configured="isProviderConfigured"
|
||||
:error="generationError"
|
||||
:show-error-details="showErrorDetails"
|
||||
@retry-get-summary="getAIAssistanceSummary"
|
||||
@retry-get-summary="retrySummaryGeneration"
|
||||
/>
|
||||
</TicketSidebarWrapper>
|
||||
</template>
|
||||
|
||||
@@ -12,13 +12,20 @@ import type { ObjectLike } from '#shared/types/utils.ts'
|
||||
import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
|
||||
import TicketSidebarContent from '#desktop/pages/ticket/components/TicketSidebar/TicketSidebarContent.vue'
|
||||
import SummarySkeleton from '#desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummary/SummarySkeleton.vue'
|
||||
import { useTicketSummary } from '#desktop/pages/ticket/composables/useTicketSummary.ts'
|
||||
import TicketSummaryCreateChecklist from '#desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent/TicketSummaryCreateChecklist.vue'
|
||||
import TicketSummaryItem from '#desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent/TicketSummaryItem.vue'
|
||||
import {
|
||||
type SummaryItem,
|
||||
TicketSummaryFeature,
|
||||
} from '#desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/types.ts'
|
||||
import type { TicketSidebarContentProps } from '#desktop/pages/ticket/types/sidebar.ts'
|
||||
|
||||
interface Props extends TicketSidebarContentProps {
|
||||
summary: Maybe<TicketAiAssistanceSummary>
|
||||
error: Maybe<AsyncExecutionError>
|
||||
showErrorDetails: boolean
|
||||
summaryHeadings: SummaryItem[]
|
||||
isProviderConfigured: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -29,8 +36,6 @@ defineEmits<{
|
||||
|
||||
const persistentStates = defineModel<ObjectLike>({ required: true })
|
||||
|
||||
const { summaryHeadings, isProviderConfigured } = useTicketSummary()
|
||||
|
||||
const errorMessage = computed(() => props.error?.message)
|
||||
|
||||
const noSummaryPossible = computed(() => {
|
||||
@@ -38,7 +43,7 @@ const noSummaryPossible = computed(() => {
|
||||
|
||||
if (!summary) return false
|
||||
|
||||
return summaryHeadings.value.every((section) => !summary[section.key]?.length)
|
||||
return props.summaryHeadings.every((section) => !summary[section.key]?.length)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -47,8 +52,9 @@ const noSummaryPossible = computed(() => {
|
||||
v-model="persistentStates.scrollPosition"
|
||||
:icon="sidebarPlugin.icon"
|
||||
:title="sidebarPlugin.title"
|
||||
variant="ai"
|
||||
>
|
||||
<section class="space-y-6">
|
||||
<section class="space-y-4.5">
|
||||
<template v-if="!isProviderConfigured">
|
||||
<CommonAlert class="self-stretch" variant="danger">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
@@ -93,35 +99,25 @@ const noSummaryPossible = computed(() => {
|
||||
{{ $t('There is not enough content yet to summarize this ticket.') }}
|
||||
</CommonAlert>
|
||||
</template>
|
||||
<template v-else-if="props.summary">
|
||||
<template v-for="heading in summaryHeadings" :key="heading.key">
|
||||
<article v-if="props.summary[heading.key]?.length">
|
||||
<CommonLabel
|
||||
class="mb-3 block! text-black! dark:text-white!"
|
||||
tag="h3"
|
||||
>{{ heading.label }}
|
||||
</CommonLabel>
|
||||
<ol
|
||||
v-if="Array.isArray(props.summary[heading.key])"
|
||||
class="space-y-1 text-gray-100 dark:text-neutral-400"
|
||||
>
|
||||
<li
|
||||
v-for="content in props.summary[heading.key]"
|
||||
:key="content"
|
||||
class="flex gap-2 ps-2 before:mt-1.5 before:h-[3px] before:w-[3px] before:shrink-0 before:rounded-full before:bg-current"
|
||||
>
|
||||
<CommonLabel tag="p">{{ content }}</CommonLabel>
|
||||
</li>
|
||||
</ol>
|
||||
<CommonLabel v-else tag="p"
|
||||
>{{ props.summary[heading.key] }}
|
||||
</CommonLabel>
|
||||
<template v-else-if="summary">
|
||||
<template v-for="item in summaryHeadings" :key="item.key">
|
||||
<article v-if="summary[item.key]?.length">
|
||||
<TicketSummaryCreateChecklist
|
||||
v-if="item.feature === TicketSummaryFeature.Checklist"
|
||||
:summary="summary[item.key] as string[]"
|
||||
:label="item.label"
|
||||
/>
|
||||
<TicketSummaryItem
|
||||
v-else
|
||||
:summary="summary[item.key]!"
|
||||
:label="item.label"
|
||||
/>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<CommonLabel
|
||||
size="small"
|
||||
class="text-stone-200! dark:text-neutral-500!"
|
||||
class="border-t border-neutral-100 pt-2 text-stone-200! dark:border-gray-900 dark:text-neutral-500!"
|
||||
tag="p"
|
||||
>{{ $t('*Be sure to check AI-generated summaries for accuracy') }}
|
||||
</CommonLabel>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import {
|
||||
NotificationTypes,
|
||||
useNotifications,
|
||||
} from '#shared/components/CommonNotifications/index.ts'
|
||||
import { MutationHandler } from '#shared/server/apollo/handler/index.ts'
|
||||
|
||||
import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
|
||||
import TicketSummaryItem from '#desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent/TicketSummaryItem.vue'
|
||||
import { useTicketInformation } from '#desktop/pages/ticket/composables/useTicketInformation.ts'
|
||||
import { useTicketSidebar } from '#desktop/pages/ticket/composables/useTicketSidebar.ts'
|
||||
import { useTicketChecklistAddMutation } from '#desktop/pages/ticket/graphql/mutations/ticketChecklistAdd.api.ts'
|
||||
import { useTicketChecklistItemsAddMutation } from '#desktop/pages/ticket/graphql/mutations/ticketChecklistItemsAdd.api.ts'
|
||||
import { useTicketChecklistItemUpsertMutation } from '#desktop/pages/ticket/graphql/mutations/ticketChecklistItemUpsert.api.ts'
|
||||
|
||||
interface Props {
|
||||
summary: string[]
|
||||
label: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { ticket } = useTicketInformation()
|
||||
|
||||
const ticketChecklist = computed(() => ticket.value?.checklist)
|
||||
|
||||
const sidebar = useTicketSidebar()
|
||||
|
||||
const { notify } = useNotifications()
|
||||
|
||||
const addNewChecklistHandler = new MutationHandler(
|
||||
useTicketChecklistAddMutation(),
|
||||
)
|
||||
|
||||
const ticketChecklistUpsertMutation = new MutationHandler(
|
||||
useTicketChecklistItemUpsertMutation(),
|
||||
)
|
||||
|
||||
const createNewChecklist = async () => {
|
||||
const response = await addNewChecklistHandler.send({
|
||||
ticketId: ticket.value!.id,
|
||||
})
|
||||
|
||||
return response?.ticketChecklistAdd?.checklist
|
||||
}
|
||||
|
||||
const createNewChecklistItem = async (text: string) => {
|
||||
const checklist = ticketChecklist.value || (await createNewChecklist())
|
||||
|
||||
if (!checklist) return
|
||||
|
||||
ticketChecklistUpsertMutation
|
||||
.send({
|
||||
checklistId: checklist.id,
|
||||
input: { text, checked: false },
|
||||
})
|
||||
.then(() =>
|
||||
notify({
|
||||
id: 'checklist-item-create',
|
||||
type: NotificationTypes.Success,
|
||||
message: __('Checklist item successfully added.'),
|
||||
}),
|
||||
)
|
||||
.catch(() =>
|
||||
notify({
|
||||
id: 'checklist-item-create-error',
|
||||
type: NotificationTypes.Error,
|
||||
message: __('Failed to add new checklist item.'),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const ticketChecklistItemsAddHandler = new MutationHandler(
|
||||
useTicketChecklistItemsAddMutation(),
|
||||
)
|
||||
|
||||
const convertAllToChecklistItems = async () => {
|
||||
const checklist = ticketChecklist.value || (await createNewChecklist())
|
||||
if (!checklist) return
|
||||
|
||||
const input = props.summary.map((text) => ({ text, checked: false }))
|
||||
|
||||
return ticketChecklistItemsAddHandler
|
||||
.send({ checklistId: checklist.id, input })
|
||||
.then(() => sidebar.switchSidebar('checklist'))
|
||||
.catch(() =>
|
||||
notify({
|
||||
id: 'checklist-item-create-error',
|
||||
type: NotificationTypes.Error,
|
||||
message: __('Failed to add new checklist items.'),
|
||||
}),
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TicketSummaryItem :label="label" :summary="summary" variant="ai">
|
||||
<template #item-trailing="{ content }">
|
||||
<CommonButton
|
||||
prefix-icon="check2-circle"
|
||||
class="col-span-2"
|
||||
:aria-label="$t('Add as checklist item')"
|
||||
@click="createNewChecklistItem(content)"
|
||||
>
|
||||
{{ $t('Add') }}
|
||||
</CommonButton>
|
||||
</template>
|
||||
|
||||
<template #trailing>
|
||||
<CommonButton
|
||||
prefix-icon="check2-all-circle"
|
||||
class="mt-0.5 self-end"
|
||||
@click="convertAllToChecklistItems"
|
||||
>{{ $t('Add all to checklist') }}</CommonButton
|
||||
>
|
||||
</template>
|
||||
</TicketSummaryItem>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
summary: string | string[]
|
||||
label: string
|
||||
variant?: 'ai'
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const variantClass = computed(() =>
|
||||
props.variant ? ['ai-stripe before:mb-3'] : [],
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col" :class="variantClass">
|
||||
<CommonLabel class="mb-3 block! text-black! dark:text-white!" tag="h3"
|
||||
>{{ label }}
|
||||
</CommonLabel>
|
||||
<ol
|
||||
v-if="Array.isArray(summary)"
|
||||
class="space-y-3 text-gray-100 dark:text-neutral-400"
|
||||
>
|
||||
<li
|
||||
v-for="content in summary"
|
||||
:key="content"
|
||||
class="grid grid-cols-[min-content_1fr] gap-x-2 gap-y-1 ps-2 before:col-start-1 before:mt-2 before:h-[3px] before:w-[3px] before:shrink-0 before:rounded-full before:bg-current"
|
||||
>
|
||||
<CommonLabel class="col-2" tag="p">{{ content }}</CommonLabel>
|
||||
|
||||
<slot name="item-trailing" :content="content" />
|
||||
</li>
|
||||
</ol>
|
||||
<CommonLabel v-else tag="p">{{ summary }} </CommonLabel>
|
||||
<slot name="trailing" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,228 @@
|
||||
// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { renderComponent } from '#tests/support/components/index.ts'
|
||||
|
||||
import { createDummyTicket } from '#shared/entities/ticket-article/__tests__/mocks/ticket.ts'
|
||||
import { convertToGraphQLId } from '#shared/graphql/utils.ts'
|
||||
|
||||
import TicketSummaryCreateChecklist from '#desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent/TicketSummaryCreateChecklist.vue'
|
||||
import { TICKET_KEY } from '#desktop/pages/ticket/composables/useTicketInformation.ts'
|
||||
import { TICKET_SIDEBAR_SYMBOL } from '#desktop/pages/ticket/composables/useTicketSidebar.ts'
|
||||
import {
|
||||
mockTicketChecklistAddMutation,
|
||||
waitForTicketChecklistAddMutationCalls,
|
||||
} from '#desktop/pages/ticket/graphql/mutations/ticketChecklistAdd.mocks.ts'
|
||||
import { waitForTicketChecklistItemsAddMutationCalls } from '#desktop/pages/ticket/graphql/mutations/ticketChecklistItemsAdd.mocks.ts'
|
||||
import { waitForTicketChecklistItemUpsertMutationCalls } from '#desktop/pages/ticket/graphql/mutations/ticketChecklistItemUpsert.mocks.ts'
|
||||
|
||||
const ticket = createDummyTicket({ checklist: null })
|
||||
|
||||
const switchSidebar = vi.fn()
|
||||
|
||||
const summary = ['Test checklist item A', 'Test checklist item B']
|
||||
|
||||
const renderTicketSummaryCreateChecklist = (
|
||||
summaryArg = summary,
|
||||
ticketArg = ticket,
|
||||
) =>
|
||||
renderComponent(TicketSummaryCreateChecklist, {
|
||||
props: {
|
||||
summary: summaryArg,
|
||||
label: 'Checklist heading',
|
||||
},
|
||||
provide: [
|
||||
[
|
||||
TICKET_KEY,
|
||||
{
|
||||
ticket: ref(ticketArg),
|
||||
ticketId: ref(ticketArg.id),
|
||||
ticketInternalId: ref(ticketArg.internalId),
|
||||
},
|
||||
],
|
||||
[
|
||||
TICKET_SIDEBAR_SYMBOL,
|
||||
{
|
||||
switchSidebar,
|
||||
},
|
||||
],
|
||||
],
|
||||
})
|
||||
|
||||
describe('TicketSummaryCreateChecklist', () => {
|
||||
it('render correctly', () => {
|
||||
const wrapper = renderTicketSummaryCreateChecklist()
|
||||
|
||||
expect(wrapper.getByRole('heading', { level: 3 })).toHaveTextContent(
|
||||
'Checklist heading',
|
||||
)
|
||||
|
||||
expect(
|
||||
wrapper.getAllByRole('button', { name: 'Add as checklist item' }).length,
|
||||
).toBe(2)
|
||||
})
|
||||
|
||||
it('converts summary item to checklist item if checklist is NOT available', async () => {
|
||||
const wrapper = renderTicketSummaryCreateChecklist()
|
||||
|
||||
const buttons = wrapper.getAllByRole('button', {
|
||||
name: 'Add as checklist item',
|
||||
})
|
||||
|
||||
await wrapper.events.click(buttons[0])
|
||||
|
||||
const mockedChecklistId = convertToGraphQLId('Checklist', 3)
|
||||
|
||||
const mockedChecklistItem = {
|
||||
id: convertToGraphQLId('Checklist::Item', 1),
|
||||
text: '',
|
||||
}
|
||||
|
||||
mockTicketChecklistAddMutation({
|
||||
ticketChecklistAdd: {
|
||||
checklist: {
|
||||
name: '',
|
||||
id: mockedChecklistId,
|
||||
items: [mockedChecklistItem],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const calls = await waitForTicketChecklistAddMutationCalls()
|
||||
|
||||
expect(calls.at(-1)?.variables).toEqual({
|
||||
ticketId: ticket.id,
|
||||
})
|
||||
|
||||
const upsertCalls = await waitForTicketChecklistItemUpsertMutationCalls()
|
||||
|
||||
expect(upsertCalls.at(-1)?.variables).toEqual({
|
||||
checklistId: expect.any(String), // mocker does not return mockedChecklistId
|
||||
input: {
|
||||
text: summary[0],
|
||||
checked: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('converts summary item to checklist item if checklist is available', async () => {
|
||||
const checklistId = convertToGraphQLId('Checklist', 23)
|
||||
const wrapper = renderTicketSummaryCreateChecklist(summary, {
|
||||
...ticket,
|
||||
checklist: {
|
||||
id: checklistId,
|
||||
incomplete: 3,
|
||||
completed: false,
|
||||
complete: 0,
|
||||
total: 3,
|
||||
},
|
||||
})
|
||||
|
||||
const buttons = wrapper.getAllByRole('button', {
|
||||
name: 'Add as checklist item',
|
||||
})
|
||||
|
||||
await wrapper.events.click(buttons[0])
|
||||
|
||||
const calls = await waitForTicketChecklistItemUpsertMutationCalls()
|
||||
|
||||
expect(calls.at(-1)?.variables).toEqual({
|
||||
checklistId,
|
||||
input: {
|
||||
text: summary[0],
|
||||
checked: false,
|
||||
},
|
||||
})
|
||||
|
||||
// :TODO if we plan to initialize CommonNotifications as an option with renderComponent
|
||||
// expect(
|
||||
// await wrapper.findByRole('alert', {
|
||||
// name: 'Checklist item successfully added.',
|
||||
// }),
|
||||
// ).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('converts all summaries to checklist items if list is NOT available', async () => {
|
||||
const wrapper = renderTicketSummaryCreateChecklist()
|
||||
|
||||
await wrapper.events.click(
|
||||
wrapper.getByRole('button', { name: 'Add all to checklist' }),
|
||||
)
|
||||
|
||||
const mockedChecklistId = convertToGraphQLId('Checklist', 3)
|
||||
|
||||
const mockedChecklistItem = {
|
||||
id: convertToGraphQLId('Checklist::Item', 1),
|
||||
text: '',
|
||||
}
|
||||
|
||||
mockTicketChecklistAddMutation({
|
||||
ticketChecklistAdd: {
|
||||
checklist: {
|
||||
name: '',
|
||||
id: mockedChecklistId,
|
||||
items: [mockedChecklistItem],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const calls = await waitForTicketChecklistAddMutationCalls()
|
||||
|
||||
expect(calls.at(-1)?.variables).toEqual({
|
||||
ticketId: ticket.id,
|
||||
})
|
||||
|
||||
const itemsAddCalls = await waitForTicketChecklistItemsAddMutationCalls()
|
||||
|
||||
expect(itemsAddCalls.at(-1)?.variables).toEqual({
|
||||
checklistId: expect.any(String), // mocker does not return mockedChecklistId
|
||||
input: [
|
||||
{
|
||||
text: summary[0],
|
||||
checked: false,
|
||||
},
|
||||
{
|
||||
text: summary[1],
|
||||
checked: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('converts all summaries to checklist items', async () => {
|
||||
const checklistId = convertToGraphQLId('Checklist', 23)
|
||||
|
||||
const wrapper = renderTicketSummaryCreateChecklist(summary, {
|
||||
...ticket,
|
||||
checklist: {
|
||||
id: checklistId,
|
||||
completed: false,
|
||||
incomplete: 3,
|
||||
complete: 0,
|
||||
total: 3,
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.events.click(
|
||||
wrapper.getByRole('button', { name: 'Add all to checklist' }),
|
||||
)
|
||||
|
||||
const itemsAddCalls = await waitForTicketChecklistItemsAddMutationCalls()
|
||||
|
||||
expect(itemsAddCalls.at(-1)?.variables).toEqual({
|
||||
checklistId,
|
||||
input: [
|
||||
{
|
||||
text: summary[0],
|
||||
checked: false,
|
||||
},
|
||||
{
|
||||
text: summary[1],
|
||||
checked: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(switchSidebar).toHaveBeenCalledWith('checklist')
|
||||
})
|
||||
})
|
||||
@@ -11,11 +11,16 @@ import { createDummyArticle } from '#shared/entities/ticket-article/__tests__/mo
|
||||
import { createDummyTicket } from '#shared/entities/ticket-article/__tests__/mocks/ticket.ts'
|
||||
import { convertToGraphQLId } from '#shared/graphql/utils.ts'
|
||||
|
||||
import { pluginFiles } from '#desktop/pages/ticket/components/TicketSidebar/plugins/index.ts'
|
||||
import plugin from '#desktop/pages/ticket/components/TicketSidebar/plugins/ticket-summary.ts'
|
||||
import TicketSidebarSummary from '#desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummary.vue'
|
||||
import { ARTICLES_INFORMATION_KEY } from '#desktop/pages/ticket/composables/useArticleContext.ts'
|
||||
import { TICKET_KEY } from '#desktop/pages/ticket/composables/useTicketInformation.ts'
|
||||
import { mockTicketAiAssistanceSummarizeMutation } from '#desktop/pages/ticket/graphql/mutations/ticketAIAssistanceSummarize.mocks.ts'
|
||||
import { TICKET_SIDEBAR_SYMBOL } from '#desktop/pages/ticket/composables/useTicketSidebar.ts'
|
||||
import {
|
||||
mockTicketAiAssistanceSummarizeMutation,
|
||||
waitForTicketAiAssistanceSummarizeMutationCalls,
|
||||
} from '#desktop/pages/ticket/graphql/mutations/ticketAIAssistanceSummarize.mocks.ts'
|
||||
|
||||
const defaultTicket = createDummyTicket()
|
||||
const testArticle = createDummyArticle({
|
||||
@@ -72,6 +77,18 @@ const renderRenderTicketSidebarSummary = (
|
||||
articlesQuery: { watchOnResult: vi.fn() },
|
||||
},
|
||||
],
|
||||
[
|
||||
TICKET_SIDEBAR_SYMBOL,
|
||||
{
|
||||
switchSidebar: vi.fn(),
|
||||
shownSidebars: ref({ 'ticket-summary': true }),
|
||||
activeSidebar: ref('ticket-summary'),
|
||||
sidebarPlugins: pluginFiles,
|
||||
hasSidebar: vi.fn(),
|
||||
showSidebar: vi.fn(),
|
||||
hideSidebar: vi.fn(),
|
||||
},
|
||||
],
|
||||
],
|
||||
global: {
|
||||
stubs: {
|
||||
@@ -301,4 +318,36 @@ describe('TicketSidebarSummary', () => {
|
||||
wrapper.getAllByLabelText('Placeholder for AI generated text'),
|
||||
).toHaveLength(16)
|
||||
})
|
||||
|
||||
it('hides feature if feature flag is disabled', async () => {
|
||||
mockApplicationConfig({
|
||||
checklist: false,
|
||||
})
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
suggestions: true,
|
||||
},
|
||||
})
|
||||
|
||||
mockTicketAiAssistanceSummarizeMutation({
|
||||
ticketAIAssistanceSummarize: ticketAIAssistanceSummarizeMock,
|
||||
})
|
||||
|
||||
const wrapper = renderRenderTicketSidebarSummary()
|
||||
|
||||
await waitForTicketAiAssistanceSummarizeMutationCalls()
|
||||
|
||||
expect(
|
||||
wrapper.queryByRole('button', { name: 'Add all to checklist' }),
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
expect(
|
||||
wrapper.queryAllByRole('button', { name: 'Add as checklist item' })
|
||||
.length,
|
||||
).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
|
||||
import type { TicketAiAssistanceSummary } from '#shared/graphql/types.ts'
|
||||
|
||||
export enum TicketSummaryFeature {
|
||||
Checklist = 'checklist',
|
||||
}
|
||||
|
||||
export interface SummaryItem {
|
||||
label: string
|
||||
key: keyof TicketAiAssistanceSummary
|
||||
feature?: TicketSummaryFeature
|
||||
active: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useApplicationStore } from '#shared/stores/application.ts'
|
||||
|
||||
import type {
|
||||
SummaryConfig,
|
||||
SummaryItem,
|
||||
} from '#desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/types.ts'
|
||||
import { useTicketInformation } from '#desktop/pages/ticket/composables/useTicketInformation.ts'
|
||||
|
||||
export const useTicketSummary = () => {
|
||||
const { config } = storeToRefs(useApplicationStore())
|
||||
|
||||
const summaryConfig = computed(
|
||||
() => config.value.ai_assistance_ticket_summary_config as SummaryConfig,
|
||||
)
|
||||
|
||||
const isProviderConfigured = computed(() => !!config.value.ai_provider)
|
||||
|
||||
const { ticket } = useTicketInformation()
|
||||
|
||||
const isEnabled = computed(
|
||||
() =>
|
||||
!!(
|
||||
ticket.value &&
|
||||
ticket.value?.state.name !== 'merged' &&
|
||||
config.value.ai_assistance_ticket_summary
|
||||
),
|
||||
)
|
||||
|
||||
const headings = computed<SummaryItem[]>(() => [
|
||||
{
|
||||
key: 'problem',
|
||||
label: __('Customer Intent'),
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
key: 'conversationSummary',
|
||||
label: __('Conversation Summary'),
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
key: 'openQuestions',
|
||||
label: __('Open Questions'),
|
||||
active: summaryConfig.value.open_questions,
|
||||
},
|
||||
{
|
||||
key: 'suggestions',
|
||||
label: __('Suggested Next Steps'),
|
||||
active: summaryConfig.value.suggestions,
|
||||
},
|
||||
])
|
||||
|
||||
const summaryHeadings = computed(() =>
|
||||
headings.value.filter((heading) => heading.active),
|
||||
)
|
||||
|
||||
return { isEnabled, isProviderConfigured, summaryHeadings }
|
||||
}
|
||||
@@ -7,8 +7,12 @@ import * as VueCompositionApi from 'vue';
|
||||
export type ReactiveFunction<TParam> = () => TParam;
|
||||
|
||||
export const TicketChecklistAddDocument = gql`
|
||||
mutation ticketChecklistAdd($ticketId: ID!, $templateId: ID) {
|
||||
ticketChecklistAdd(ticketId: $ticketId, templateId: $templateId) {
|
||||
mutation ticketChecklistAdd($ticketId: ID!, $templateId: ID, $createFirstItem: Boolean) {
|
||||
ticketChecklistAdd(
|
||||
ticketId: $ticketId
|
||||
templateId: $templateId
|
||||
createFirstItem: $createFirstItem
|
||||
) {
|
||||
checklist {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mutation ticketChecklistAdd($ticketId: ID!, $templateId: ID) {
|
||||
ticketChecklistAdd(ticketId: $ticketId, templateId: $templateId) {
|
||||
mutation ticketChecklistAdd($ticketId: ID!, $templateId: ID, $createFirstItem: Boolean) {
|
||||
ticketChecklistAdd(ticketId: $ticketId, templateId: $templateId, createFirstItem: $createFirstItem) {
|
||||
checklist {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import gql from 'graphql-tag';
|
||||
import * as VueApolloComposable from '@vue/apollo-composable';
|
||||
import * as VueCompositionApi from 'vue';
|
||||
export type ReactiveFunction<TParam> = () => TParam;
|
||||
|
||||
export const TicketChecklistItemsAddDocument = gql`
|
||||
mutation ticketChecklistItemsAdd($checklistId: ID!, $input: [TicketChecklistItemInput!]!) {
|
||||
ticketChecklistItemsAdd(checklistId: $checklistId, input: $input) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export function useTicketChecklistItemsAddMutation(options: VueApolloComposable.UseMutationOptions<Types.TicketChecklistItemsAddMutation, Types.TicketChecklistItemsAddMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<Types.TicketChecklistItemsAddMutation, Types.TicketChecklistItemsAddMutationVariables>> = {}) {
|
||||
return VueApolloComposable.useMutation<Types.TicketChecklistItemsAddMutation, Types.TicketChecklistItemsAddMutationVariables>(TicketChecklistItemsAddDocument, options);
|
||||
}
|
||||
export type TicketChecklistItemsAddMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<Types.TicketChecklistItemsAddMutation, Types.TicketChecklistItemsAddMutationVariables>;
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation ticketChecklistItemsAdd ($checklistId: ID!, $input: [TicketChecklistItemInput!]!){
|
||||
ticketChecklistItemsAdd(checklistId: $checklistId, input: $input) {
|
||||
success
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import * as Mocks from '#tests/graphql/builders/mocks.ts'
|
||||
import * as Operations from './ticketChecklistItemsAdd.api.ts'
|
||||
import * as ErrorTypes from '#shared/types/error.ts'
|
||||
|
||||
export function mockTicketChecklistItemsAddMutation(defaults: Mocks.MockDefaultsValue<Types.TicketChecklistItemsAddMutation, Types.TicketChecklistItemsAddMutationVariables>) {
|
||||
return Mocks.mockGraphQLResult(Operations.TicketChecklistItemsAddDocument, defaults)
|
||||
}
|
||||
|
||||
export function waitForTicketChecklistItemsAddMutationCalls() {
|
||||
return Mocks.waitForGraphQLMockCalls<Types.TicketChecklistItemsAddMutation>(Operations.TicketChecklistItemsAddDocument)
|
||||
}
|
||||
|
||||
export function mockTicketChecklistItemsAddMutationError(message: string, extensions: {type: ErrorTypes.GraphQLErrorTypes }) {
|
||||
return Mocks.mockGraphQLResultWithError(Operations.TicketChecklistItemsAddDocument, message, extensions);
|
||||
}
|
||||
@@ -252,6 +252,19 @@
|
||||
@apply after:ms-1 after:inline-block after:font-extrabold after:text-yellow-600 after:content-['•'];
|
||||
}
|
||||
|
||||
@utility ai-stripe {
|
||||
&::before {
|
||||
content: '';
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-image: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--color-blue-800),
|
||||
var(--color-pink-200)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Uses to be in the @layer utilities */
|
||||
@media print {
|
||||
[data-print-mode='true'] body {
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
--color-red-600: #86270c;
|
||||
--color-red-900: #220c06;
|
||||
--color-pink-100: #efd9d2;
|
||||
--color-pink-200: #d75b83;
|
||||
--color-pink-300: #ea4d84;
|
||||
--color-pink-500: #ff006b;
|
||||
|
||||
|
||||
@@ -1560,6 +1560,8 @@ export type Mutations = {
|
||||
ticketChecklistItemOrderUpdate?: Maybe<TicketChecklistItemOrderUpdatePayload>;
|
||||
/** Update or create a ticket checklist item. */
|
||||
ticketChecklistItemUpsert?: Maybe<TicketChecklistItemUpsertPayload>;
|
||||
/** Add checklist items. */
|
||||
ticketChecklistItemsAdd?: Maybe<TicketChecklistItemsAddPayload>;
|
||||
/** Update title of the ticket checklist. */
|
||||
ticketChecklistTitleUpdate?: Maybe<TicketChecklistTitleUpdatePayload>;
|
||||
/** Create a new ticket. */
|
||||
@@ -1900,6 +1902,7 @@ export type MutationsTicketArticleRetrySecurityProcessArgs = {
|
||||
|
||||
/** All available mutations */
|
||||
export type MutationsTicketChecklistAddArgs = {
|
||||
createFirstItem?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
templateId?: InputMaybe<Scalars['ID']['input']>;
|
||||
ticketId: Scalars['ID']['input'];
|
||||
};
|
||||
@@ -1933,6 +1936,13 @@ export type MutationsTicketChecklistItemUpsertArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** All available mutations */
|
||||
export type MutationsTicketChecklistItemsAddArgs = {
|
||||
checklistId: Scalars['ID']['input'];
|
||||
input: Array<TicketChecklistItemInput>;
|
||||
};
|
||||
|
||||
|
||||
/** All available mutations */
|
||||
export type MutationsTicketChecklistTitleUpdateArgs = {
|
||||
checklistId: Scalars['ID']['input'];
|
||||
@@ -4037,11 +4047,22 @@ export type TicketChecklistItemOrderUpdatePayload = {
|
||||
export type TicketChecklistItemUpsertPayload = {
|
||||
__typename?: 'TicketChecklistItemUpsertPayload';
|
||||
/** Updated or created checklist item. */
|
||||
checklistItem?: Maybe<ChecklistItem>;
|
||||
checklistItem: ChecklistItem;
|
||||
/** Errors encountered during execution of the mutation. */
|
||||
errors?: Maybe<Array<UserError>>;
|
||||
};
|
||||
|
||||
/** Autogenerated return type of TicketChecklistItemsAdd. */
|
||||
export type TicketChecklistItemsAddPayload = {
|
||||
__typename?: 'TicketChecklistItemsAddPayload';
|
||||
/** Updated checklist. */
|
||||
checklist: Checklist;
|
||||
/** Errors encountered during execution of the mutation. */
|
||||
errors?: Maybe<Array<UserError>>;
|
||||
/** Was the mutation successful? */
|
||||
success: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
/** Autogenerated return type of TicketChecklistTitleUpdate. */
|
||||
export type TicketChecklistTitleUpdatePayload = {
|
||||
__typename?: 'TicketChecklistTitleUpdatePayload';
|
||||
@@ -6171,6 +6192,7 @@ export type TicketAiAssistanceSummarizeMutation = { __typename?: 'Mutations', ti
|
||||
export type TicketChecklistAddMutationVariables = Exact<{
|
||||
ticketId: Scalars['ID']['input'];
|
||||
templateId?: InputMaybe<Scalars['ID']['input']>;
|
||||
createFirstItem?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -6206,7 +6228,15 @@ export type TicketChecklistItemUpsertMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type TicketChecklistItemUpsertMutation = { __typename?: 'Mutations', ticketChecklistItemUpsert?: { __typename?: 'TicketChecklistItemUpsertPayload', checklistItem?: { __typename?: 'ChecklistItem', id: string, text: string, checked: boolean, ticketReference?: { __typename?: 'TicketReference', ticket?: { __typename?: 'Ticket', id: string, internalId: number, number: string, title: string, stateColorCode: EnumTicketStateColorCode, state: { __typename?: 'TicketState', id: string, name: string } } | null } | null } | null, errors?: Array<{ __typename?: 'UserError', message: string, field?: string | null, exception?: EnumUserErrorException | null }> | null } | null };
|
||||
export type TicketChecklistItemUpsertMutation = { __typename?: 'Mutations', ticketChecklistItemUpsert?: { __typename?: 'TicketChecklistItemUpsertPayload', checklistItem: { __typename?: 'ChecklistItem', id: string, text: string, checked: boolean, ticketReference?: { __typename?: 'TicketReference', ticket?: { __typename?: 'Ticket', id: string, internalId: number, number: string, title: string, stateColorCode: EnumTicketStateColorCode, state: { __typename?: 'TicketState', id: string, name: string } } | null } | null }, errors?: Array<{ __typename?: 'UserError', message: string, field?: string | null, exception?: EnumUserErrorException | null }> | null } | null };
|
||||
|
||||
export type TicketChecklistItemsAddMutationVariables = Exact<{
|
||||
checklistId: Scalars['ID']['input'];
|
||||
input: Array<TicketChecklistItemInput> | TicketChecklistItemInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type TicketChecklistItemsAddMutation = { __typename?: 'Mutations', ticketChecklistItemsAdd?: { __typename?: 'TicketChecklistItemsAddPayload', success: boolean } | null };
|
||||
|
||||
export type TicketChecklistTitleUpdateMutationVariables = Exact<{
|
||||
checklistId: Scalars['ID']['input'];
|
||||
|
||||
@@ -6,20 +6,23 @@ module Gql::Mutations
|
||||
|
||||
argument :ticket_id, GraphQL::Types::ID, loads: Gql::Types::TicketType, description: 'Ticket to create the new checklist for.'
|
||||
argument :template_id, GraphQL::Types::ID, required: false, description: 'Checklist template ID to base the ticket checklist on.'
|
||||
argument :create_first_item, GraphQL::Types::Boolean, required: false, description: 'Create the first item in the checklist (only if no template is used).'
|
||||
|
||||
field :checklist, Gql::Types::ChecklistType, null: true, description: 'Created checklist'
|
||||
|
||||
def authorized?(ticket:, template_id: nil)
|
||||
def authorized?(ticket:, create_first_item: false, template_id: nil)
|
||||
Setting.get('checklist') && pundit_authorized?(ticket, :agent_update_access?)
|
||||
end
|
||||
|
||||
def resolve(ticket:, template_id: nil)
|
||||
def resolve(ticket:, create_first_item: false, template_id: nil)
|
||||
checklist = if template_id
|
||||
template = Gql::ZammadSchema.verified_object_from_id(template_id, type: ::ChecklistTemplate)
|
||||
|
||||
Checklist.create_from_template!(ticket, template)
|
||||
else
|
||||
elsif create_first_item
|
||||
Checklist.create_fresh!(ticket)
|
||||
else
|
||||
Checklist.create!(ticket:)
|
||||
end
|
||||
|
||||
{ checklist: }
|
||||
|
||||
@@ -4,11 +4,11 @@ module Gql::Mutations
|
||||
class Ticket::Checklist::ItemUpsert < Ticket::Checklist::Base
|
||||
description 'Update or create a ticket checklist item.'
|
||||
|
||||
argument :checklist_id, GraphQL::Types::ID, required: true, loads: Gql::Types::ChecklistType, description: 'ID of the ticket checklist to update or create an item for.'
|
||||
argument :checklist_id, GraphQL::Types::ID, loads: Gql::Types::ChecklistType, description: 'ID of the ticket checklist to update or create an item for.'
|
||||
argument :checklist_item_id, GraphQL::Types::ID, required: false, loads: Gql::Types::Checklist::ItemType, description: 'ID of the ticket checklist item to update.'
|
||||
argument :input, Gql::Types::Input::Ticket::Checklist::ItemInputType, required: true, description: 'Input field values of the ticket checklist item.'
|
||||
argument :input, Gql::Types::Input::Ticket::Checklist::ItemInputType, description: 'Input field values of the ticket checklist item.'
|
||||
|
||||
field :checklist_item, Gql::Types::Checklist::ItemType, null: true, description: 'Updated or created checklist item.'
|
||||
field :checklist_item, Gql::Types::Checklist::ItemType, null: false, description: 'Updated or created checklist item.'
|
||||
|
||||
def authorized?(checklist:, input:, checklist_item: nil)
|
||||
if checklist_item
|
||||
|
||||
28
app/graphql/gql/mutations/ticket/checklist/items_add.rb
Normal file
28
app/graphql/gql/mutations/ticket/checklist/items_add.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Gql::Mutations
|
||||
class Ticket::Checklist::ItemsAdd < Ticket::Checklist::Base
|
||||
description 'Add checklist items.'
|
||||
|
||||
argument :checklist_id, GraphQL::Types::ID, loads: Gql::Types::ChecklistType, description: 'ID of the ticket checklist to update or create an item for.'
|
||||
argument :input, [Gql::Types::Input::Ticket::Checklist::ItemInputType], description: 'Input field values of the ticket checklist item.'
|
||||
|
||||
field :success, Boolean, null: false, description: 'Was the mutation successful?'
|
||||
field :checklist, Gql::Types::ChecklistType, null: false, description: 'Updated checklist.'
|
||||
|
||||
def authorized?(checklist:, input:)
|
||||
pundit_authorized?(Checklist::Item.new(checklist:), :create?)
|
||||
end
|
||||
|
||||
def resolve(checklist:, input:)
|
||||
input.each do |item_input|
|
||||
checklist.items.create!(item_input.to_h)
|
||||
end
|
||||
|
||||
{
|
||||
success: true,
|
||||
checklist: checklist.reload
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -10010,6 +10010,16 @@
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "createFirstItem",
|
||||
"description": "Create the first item in the checklist (only if no template is used).",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
@@ -10188,6 +10198,55 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "ticketChecklistItemsAdd",
|
||||
"description": "Add checklist items.",
|
||||
"args": [
|
||||
{
|
||||
"name": "checklistId",
|
||||
"description": "ID of the ticket checklist to update or create an item for.",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "input",
|
||||
"description": "Input field values of the ticket checklist item.",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "TicketChecklistItemInput",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "TicketChecklistItemsAddPayload",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "ticketChecklistTitleUpdate",
|
||||
"description": "Update title of the ticket checklist.",
|
||||
@@ -22229,9 +22288,13 @@
|
||||
"description": "Updated or created checklist item.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "ChecklistItem",
|
||||
"ofType": null
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "ChecklistItem",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
@@ -22262,6 +22325,69 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "TicketChecklistItemsAddPayload",
|
||||
"description": "Autogenerated return type of TicketChecklistItemsAdd.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "checklist",
|
||||
"description": "Updated checklist.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Checklist",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "errors",
|
||||
"description": "Errors encountered during execution of the mutation.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "UserError",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "success",
|
||||
"description": "Was the mutation successful?",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "TicketChecklistTitleUpdatePayload",
|
||||
|
||||
@@ -7,6 +7,10 @@ class Controllers::ChecklistItemsControllerPolicy < Controllers::ApplicationCont
|
||||
.create?
|
||||
end
|
||||
|
||||
def create_bulk?
|
||||
create?
|
||||
end
|
||||
|
||||
def show?
|
||||
Checklist::ItemPolicy
|
||||
.new(user, checklist_item)
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
Zammad::Application.routes.draw do
|
||||
scope Rails.configuration.api_path do
|
||||
resources :checklists, only: %i[show create update destroy]
|
||||
resources :checklist_items, only: %i[create update destroy show]
|
||||
resources :checklist_items, only: %i[create update destroy show] do
|
||||
post :create_bulk, on: :collection
|
||||
end
|
||||
resources :checklist_templates, only: %i[index show create update destroy]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -473,8 +473,8 @@ msgstr ""
|
||||
msgid "**Service Level Agreements**, abbreviated **SLAs**, help you to meet specific response times for your customers' requests. This way you can define goals such as answering every inquiry within eight hours. If you are at risk of missing this target, Zammad will alert you.\n\nYou can define targets for three different metrics: **response time** (time between the creation of a ticket and the first reaction of an agent), **update time** (time between a customer's request and an agent's reaction), and **solution time** (time between creating and closing a ticket).\n\nAny escalated tickets (i.e. tickets that have missed the defined target) are displayed in a separate view in your overviews. You can also configure **email notifications**."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_ticket_summary.jst.eco:37
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:126
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_ticket_summary.jst.eco:62
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:122
|
||||
msgid "*Be sure to check AI-generated summaries for accuracy"
|
||||
msgstr ""
|
||||
|
||||
@@ -1008,7 +1008,9 @@ msgstr ""
|
||||
#: app/assets/javascripts/app/views/profile/linked_accounts.jst.eco:15
|
||||
#: app/assets/javascripts/app/views/tag/index.jst.eco:25
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_checklist/show.jst.eco:19
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_ticket_summary.jst.eco:34
|
||||
#: app/frontend/apps/desktop/components/Form/fields/FieldGroupPermissions/FieldGroupPermissionsInput.vue:278
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent/TicketSummaryCreateChecklist.vue:106
|
||||
#: app/frontend/shared/form/i18n/locales.ts:25
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
@@ -1033,7 +1035,7 @@ msgstr ""
|
||||
msgid "Add Certificate"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:452
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:453
|
||||
msgid "Add Empty Checklist"
|
||||
msgstr ""
|
||||
|
||||
@@ -1107,10 +1109,20 @@ msgstr ""
|
||||
msgid "Add alarm to pending reminder and escalated tickets."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_ticket_summary.jst.eco:54
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent/TicketSummaryCreateChecklist.vue:118
|
||||
msgid "Add all to checklist"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/shared/form/i18n/locales.ts:74
|
||||
msgid "Add all values"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_ticket_summary.jst.eco:34
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent/TicketSummaryCreateChecklist.vue:106
|
||||
msgid "Add as checklist item"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/channel/form.jst.eco:112
|
||||
msgid "Add attachment option to upload."
|
||||
msgstr ""
|
||||
@@ -2843,7 +2855,7 @@ msgstr ""
|
||||
msgid "Check this box if you want to customise how options are sorted. If the box is not checked, values are sorted in alphabetical order."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist.coffee:51
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist.coffee:52
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/plugins/checklist.ts:11
|
||||
msgid "Checklist"
|
||||
msgstr ""
|
||||
@@ -2866,6 +2878,11 @@ msgstr ""
|
||||
msgid "Checklist Template items are limited to 100 items per checklist."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket_summary.coffee:93
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent/TicketSummaryCreateChecklist.vue:65
|
||||
msgid "Checklist item successfully added."
|
||||
msgstr ""
|
||||
|
||||
#: app/models/checklist/item.rb:108
|
||||
msgid "Checklist items are limited to 100 items per checklist."
|
||||
msgstr ""
|
||||
@@ -3484,7 +3501,7 @@ msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/ticket_summary.coffee:34
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket_summary.coffee:4
|
||||
#: app/frontend/apps/desktop/pages/ticket/composables/useTicketSummary.ts:41
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummary.vue:74
|
||||
msgid "Conversation Summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -4004,7 +4021,7 @@ msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/ticket_summary.coffee:27
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket_summary.coffee:3
|
||||
#: app/frontend/apps/desktop/pages/ticket/composables/useTicketSummary.ts:36
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummary.vue:69
|
||||
msgid "Customer Intent"
|
||||
msgstr ""
|
||||
|
||||
@@ -6526,11 +6543,16 @@ msgstr ""
|
||||
msgid "Failed Tasks"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:146
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:147
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent/TicketSummaryCreateChecklist.vue:72
|
||||
msgid "Failed to add new checklist item."
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:167
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent/TicketSummaryCreateChecklist.vue:94
|
||||
msgid "Failed to add new checklist items."
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:168
|
||||
msgid "Failed to delete checklist item."
|
||||
msgstr ""
|
||||
|
||||
@@ -6548,7 +6570,7 @@ msgstr ""
|
||||
msgid "Failed to roll back the migration of the channel!"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:153
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:154
|
||||
msgid "Failed to save checklist order."
|
||||
msgstr ""
|
||||
|
||||
@@ -6560,7 +6582,7 @@ msgstr ""
|
||||
msgid "Failed to set up QR code. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:160
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:161
|
||||
msgid "Failed to update checklist item."
|
||||
msgstr ""
|
||||
|
||||
@@ -10265,7 +10287,7 @@ msgid "No %s configured."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_ticket_summary.jst.eco:15
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:58
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:64
|
||||
msgid "No AI provider is currently set up. Please contact your administrator."
|
||||
msgstr ""
|
||||
|
||||
@@ -10357,7 +10379,7 @@ msgid "No certificate found."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_checklist/start.jst.eco:3
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:465
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:466
|
||||
msgid "No checklist added to this ticket yet."
|
||||
msgstr ""
|
||||
|
||||
@@ -10991,7 +11013,7 @@ msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/ticket_summary.coffee:41
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket_summary.coffee:5
|
||||
#: app/frontend/apps/desktop/pages/ticket/composables/useTicketSummary.ts:46
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummary.vue:79
|
||||
msgid "Open Questions"
|
||||
msgstr ""
|
||||
|
||||
@@ -12430,7 +12452,7 @@ msgid "Remove authenticator app"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist.coffee:21
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:405
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:406
|
||||
msgid "Remove checklist"
|
||||
msgstr ""
|
||||
|
||||
@@ -12484,7 +12506,7 @@ msgid "Removes the shadows for a flat look."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist.coffee:16
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:398
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklist/TicketSidebarChecklistContent.vue:399
|
||||
msgid "Rename checklist"
|
||||
msgstr ""
|
||||
|
||||
@@ -12730,7 +12752,7 @@ msgstr ""
|
||||
#: app/frontend/apps/desktop/components/TwoFactor/TwoFactorConfiguration/TwoFactorConfigurationSecurityKeys.vue:66
|
||||
#: app/frontend/apps/desktop/pages/authentication/components/LoginTwoFactor.vue:177
|
||||
#: app/frontend/apps/desktop/pages/authentication/views/AdminPasswordAuth.vue:140
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:87
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:93
|
||||
#: app/frontend/apps/mobile/pages/authentication/components/LoginTwoFactor.vue:177
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
@@ -14511,11 +14533,11 @@ msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/ticket_summary.coffee:47
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket_summary.coffee:6
|
||||
#: app/frontend/apps/desktop/pages/ticket/composables/useTicketSummary.ts:51
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummary.vue:84
|
||||
msgid "Suggested Next Steps"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket_summary.coffee:38
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket_summary.coffee:41
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/plugins/ticket-summary.ts:11
|
||||
msgid "Summary"
|
||||
msgstr ""
|
||||
@@ -15828,7 +15850,7 @@ msgstr ""
|
||||
msgid "The start date must precede or match end date."
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:72
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:78
|
||||
msgid "The summary could not be generated. Please try again later or contact your administrator."
|
||||
msgstr ""
|
||||
|
||||
@@ -16075,7 +16097,7 @@ msgid "There is no valid PGP email signer."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_ticket_summary.jst.eco:19
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:93
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:99
|
||||
msgid "There is not enough content yet to summarize this ticket."
|
||||
msgstr ""
|
||||
|
||||
@@ -17535,6 +17557,18 @@ msgstr ""
|
||||
msgid "URL (AJAX endpoint)"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket_summary.coffee:140
|
||||
msgid "Unable to add all checklist items."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket_summary.coffee:98
|
||||
msgid "Unable to add checklist item."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket_summary.coffee:65
|
||||
msgid "Unable to create checklist."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_ticket_summary.jst.eco:5
|
||||
msgid "Unable to generate summary, try again later or contact your admin."
|
||||
msgstr ""
|
||||
@@ -19063,8 +19097,8 @@ msgstr ""
|
||||
msgid "Zammad Helpdesk"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_ticket_summary.jst.eco:40
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:136
|
||||
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_ticket_summary.jst.eco:65
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:132
|
||||
msgid "Zammad Smart Assist is generating the summary for you…"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
</symbol><symbol id="icon-chat" viewBox="0 0 24 24">
|
||||
<title>chat</title>
|
||||
<path d="M2 5.006C2 3.898 2.898 3 3.99 3h16.02C21.108 3 22 3.897 22 5.006v9.988A2.003 2.003 0 0 1 20.01 17H3.99C2.892 17 2 16.103 2 14.994V5.006ZM12 17l6 4v-4h-6Z" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-check2-square" viewBox="0 0 16 16">
|
||||
<path d="M3 14.5A1.5 1.5 0 0 1 1.5 13V3A1.5 1.5 0 0 1 3 1.5h8a.5.5 0 0 1 0 1H3a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5h10a.5.5 0 0 0 .5-.5V8a.5.5 0 0 1 1 0v5a1.5 1.5 0 0 1-1.5 1.5H3Z"/>
|
||||
<path d="m8.354 10.354 7-7a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0Z"/>
|
||||
</symbol><symbol id="icon-checkbox-checked-readonly" viewBox="0 0 11 11">
|
||||
<title>checkbox-checked-readonly</title>
|
||||
<g fill-rule="evenodd">
|
||||
|
||||
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 115 KiB |
5
public/assets/images/icons/check2-square.svg
Normal file
5
public/assets/images/icons/check2-square.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="check2-square" d="M3 14.5C2.17157 14.5 1.5 13.8284 1.5 13V3C1.5 2.17157 2.17157 1.5 3 1.5H11C11.2761 1.5 11.5 1.72386 11.5 2C11.5 2.27614 11.2761 2.5 11 2.5H3C2.72386 2.5 2.5 2.72386 2.5 3V13C2.5 13.2761 2.72386 13.5 3 13.5H13C13.2761 13.5 13.5 13.2761 13.5 13V8C13.5 7.72386 13.7239 7.5 14 7.5C14.2761 7.5 14.5 7.72386 14.5 8V13C14.5 13.8284 13.8284 14.5 13 14.5H3Z" />
|
||||
<path d="M8.35355 10.3536L15.3536 3.35355C15.5488 3.15829 15.5488 2.84171 15.3536 2.64645C15.1583 2.45118 14.8417 2.45118 14.6464 2.64645L8 9.29289L5.35355 6.64645C5.15829 6.45118 4.84171 6.45118 4.64645 6.64645C4.45118 6.84171 4.45118 7.15829 4.64645 7.35355L7.64645 10.3536C7.84171 10.5488 8.15829 10.5488 8.35355 10.3536Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 845 B |
@@ -9,8 +9,8 @@ RSpec.describe Gql::Mutations::Ticket::Checklist::Add, current_user_id: 1, type:
|
||||
|
||||
let(:query) do
|
||||
<<~QUERY
|
||||
mutation ticketChecklistAdd($ticketId: ID!, $templateId: ID) {
|
||||
ticketChecklistAdd(ticketId: $ticketId, templateId: $templateId) {
|
||||
mutation ticketChecklistAdd($ticketId: ID!, $createFirstItem: Boolean, $templateId: ID) {
|
||||
ticketChecklistAdd(ticketId: $ticketId, createFirstItem: $createFirstItem, templateId: $templateId) {
|
||||
checklist {
|
||||
id
|
||||
name
|
||||
@@ -28,7 +28,8 @@ RSpec.describe Gql::Mutations::Ticket::Checklist::Add, current_user_id: 1, type:
|
||||
QUERY
|
||||
end
|
||||
|
||||
let(:variables) { { ticketId: gql.id(ticket) } }
|
||||
let(:create_first_item) { true }
|
||||
let(:variables) { { ticketId: gql.id(ticket), createFirstItem: create_first_item } }
|
||||
|
||||
let(:response) do
|
||||
{
|
||||
@@ -69,7 +70,23 @@ RSpec.describe Gql::Mutations::Ticket::Checklist::Add, current_user_id: 1, type:
|
||||
end
|
||||
|
||||
context 'with authenticated session', authenticated_as: :agent do
|
||||
it_behaves_like 'creating the ticket checklist'
|
||||
|
||||
context 'with create_first_item' do
|
||||
it_behaves_like 'creating the ticket checklist'
|
||||
end
|
||||
|
||||
context 'without create_first_item' do
|
||||
let(:create_first_item) { false }
|
||||
let(:response) do
|
||||
{
|
||||
'id' => a_kind_of(String),
|
||||
'name' => '',
|
||||
'items' => [],
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'creating the ticket checklist'
|
||||
end
|
||||
|
||||
context 'with disabled checklist feature' do
|
||||
let(:setup) do
|
||||
|
||||
109
spec/graphql/gql/mutations/ticket/checklist/items_add_spec.rb
Normal file
109
spec/graphql/gql/mutations/ticket/checklist/items_add_spec.rb
Normal file
@@ -0,0 +1,109 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Gql::Mutations::Ticket::Checklist::ItemsAdd, current_user_id: 1, type: :graphql do
|
||||
let(:group) { create(:group) }
|
||||
let(:agent) { create(:agent, groups: [group]) }
|
||||
let(:ticket) { create(:ticket, group: group) }
|
||||
let(:checklist) { create(:checklist, ticket: ticket) }
|
||||
let(:input) { [{ 'text' => 'foobar', 'checked' => true }] }
|
||||
|
||||
let(:query) do
|
||||
<<~QUERY
|
||||
mutation ticketChecklistItemsAdd($checklistId: ID!, $input: [TicketChecklistItemInput!]!) {
|
||||
ticketChecklistItemsAdd(checklistId: $checklistId, input: $input) {
|
||||
success
|
||||
checklist {
|
||||
items {
|
||||
text
|
||||
checked
|
||||
}
|
||||
}
|
||||
errors {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
QUERY
|
||||
end
|
||||
|
||||
let(:variables) { { checklistId: gql.id(checklist), input: input } }
|
||||
|
||||
before do
|
||||
setup if defined?(setup)
|
||||
gql.execute(query, variables: variables)
|
||||
end
|
||||
|
||||
shared_examples 'creating the ticket checklist items' do
|
||||
it 'creates the ticket checklist items' do
|
||||
expect(gql.result.data).to include(
|
||||
'success' => true,
|
||||
'checklist' => {
|
||||
'items' => include(
|
||||
{
|
||||
'text' => variables[:input].first['text'],
|
||||
'checked' => variables[:input].first['checked'] || false,
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'raising an error' do |error_type|
|
||||
it 'raises an error' do
|
||||
expect(gql.result.error_type).to eq(error_type)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with authenticated session', authenticated_as: :agent do
|
||||
it_behaves_like 'creating the ticket checklist items'
|
||||
|
||||
context 'with disabled checklist feature' do
|
||||
let(:setup) do
|
||||
Setting.set('checklist', false)
|
||||
end
|
||||
|
||||
it_behaves_like 'raising an error', Exceptions::Forbidden
|
||||
end
|
||||
|
||||
context 'when providing both checked state and text' do
|
||||
let(:input) { [{ 'checked' => true, 'text' => '' }] }
|
||||
|
||||
it_behaves_like 'creating the ticket checklist items'
|
||||
end
|
||||
|
||||
context 'when providing text value only' do
|
||||
let(:input) { [{ 'text' => 'foobar' }] }
|
||||
|
||||
it_behaves_like 'creating the ticket checklist items'
|
||||
end
|
||||
|
||||
context 'without access to the ticket' do
|
||||
let(:agent) { create(:agent) }
|
||||
|
||||
it_behaves_like 'raising an error', Exceptions::Forbidden
|
||||
end
|
||||
|
||||
context 'with ticket read permission' do
|
||||
let(:agent) { create(:agent, groups: [group], group_names_access_map: { group.name => 'read' }) }
|
||||
|
||||
it_behaves_like 'raising an error', Exceptions::Forbidden
|
||||
end
|
||||
|
||||
context 'with ticket read+change permissions' do
|
||||
let(:agent) { create(:agent, groups: [group], group_names_access_map: { group.name => %w[read change] }) }
|
||||
|
||||
it_behaves_like 'creating the ticket checklist items'
|
||||
end
|
||||
|
||||
context 'when ticket checklist does not exist' do
|
||||
let(:variables) { { checklistId: 'gid://Zammad/Checklist/0', input: input } }
|
||||
|
||||
it_behaves_like 'raising an error', ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'graphql responds with error if unauthenticated'
|
||||
end
|
||||
@@ -44,6 +44,17 @@ RSpec.describe 'Checklist Item', authenticated_as: :agent_1, current_user_id: 1,
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'does bulk create checklist items', :aggregate_failures do
|
||||
post '/api/v1/checklist_items/create_bulk', params: { checklist_id: checklist_1.id, items: [{ text: SecureRandom.uuid }] }, as: :json
|
||||
expect(response).to have_http_status(:created)
|
||||
expect(json_response).to include('success' => true, 'checklist_item_ids' => [Checklist::Item.last.id])
|
||||
end
|
||||
|
||||
it 'does not bulk create checklist items' do
|
||||
post '/api/v1/checklist_items/create_bulk', params: { checklist_id: checklist_2.id, items: [{ text: SecureRandom.uuid }] }, as: :json
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'does update checklist items', :aggregate_failures do
|
||||
put "/api/v1/checklist_items/#{checklist_1.items.first.id}", params: { id: checklist_1.items.first.id, text: SecureRandom.uuid }, as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
@@ -35,10 +35,18 @@ RSpec.describe 'Checklist', authenticated_as: :agent_1, current_user_id: 1, type
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'does create checklist', :aggregate_failures do
|
||||
it 'does create checklist with create_first_item', :aggregate_failures do
|
||||
post '/api/v1/checklists', params: { name: SecureRandom.uuid, ticket_id: ticket_1_empty.id, create_first_item: true }, as: :json
|
||||
expect(response).to have_http_status(:created)
|
||||
expect(json_response).to include('id' => Checklist.last.id)
|
||||
expect(Checklist.last.items.count).to eq(1)
|
||||
end
|
||||
|
||||
it 'does create checklist without create_first_item', :aggregate_failures do
|
||||
post '/api/v1/checklists', params: { name: SecureRandom.uuid, ticket_id: ticket_1_empty.id }, as: :json
|
||||
expect(response).to have_http_status(:created)
|
||||
expect(json_response).to include('id' => Checklist.last.id)
|
||||
expect(Checklist.last.items.count).to eq(0)
|
||||
end
|
||||
|
||||
it 'does not create checklist' do
|
||||
|
||||
@@ -8,10 +8,12 @@ RSpec.describe 'Ticket Summary', authenticated_as: :authenticate, type: :system
|
||||
let(:article) { create(:ticket_article, ticket:) }
|
||||
let(:ai_provider) { 'zammad_ai' }
|
||||
let(:ai_assistance_ticket_summary) { true }
|
||||
let(:checklist) { true }
|
||||
|
||||
def authenticate
|
||||
Setting.set('ai_provider', ai_provider)
|
||||
Setting.set('ai_assistance_ticket_summary', ai_assistance_ticket_summary)
|
||||
Setting.set('checklist', checklist)
|
||||
|
||||
article
|
||||
|
||||
@@ -23,11 +25,11 @@ RSpec.describe 'Ticket Summary', authenticated_as: :authenticate, type: :system
|
||||
allow(AI::Service::TicketSummarize)
|
||||
.to receive(:cache_key).and_return(initial_cache_key, :noop, updated_cache_key)
|
||||
|
||||
Rails.cache.write(initial_cache_key, { 'summary' => initial_summary })
|
||||
Rails.cache.write(updated_cache_key, { 'summary' => updated_summary })
|
||||
Rails.cache.write(initial_cache_key, { 'summary' => initial_summary, 'suggestions' => (initial_suggestions if defined?(initial_suggestions)) }.compact)
|
||||
Rails.cache.write(updated_cache_key, { 'summary' => updated_summary, 'suggestions' => (updated_suggestions if defined?(updated_suggestions)) }.compact)
|
||||
|
||||
allow_any_instance_of(Service::Ticket::AIAssistance::Summarize)
|
||||
.to receive(:execute).and_return({ 'summary' => updated_summary })
|
||||
.to receive(:execute).and_return({ 'summary' => updated_summary, 'suggestions' => (updated_suggestions if defined?(updated_suggestions)) }.compact)
|
||||
end
|
||||
|
||||
visit "ticket/zoom/#{ticket.id}"
|
||||
@@ -35,16 +37,20 @@ RSpec.describe 'Ticket Summary', authenticated_as: :authenticate, type: :system
|
||||
|
||||
describe 'Sidebar' do
|
||||
context 'when ai_provider is set' do
|
||||
let(:initial_summary) { Faker::Lorem.sentence }
|
||||
let(:updated_summary) { Faker::Lorem.sentence }
|
||||
let(:initial_summary) { Faker::Lorem.unique.sentence }
|
||||
let(:updated_summary) { Faker::Lorem.unique.sentence }
|
||||
let(:initial_cache_key) { "ticket_summary_#{ticket.id}" }
|
||||
let(:updated_cache_key) { "ticket_summary_#{ticket.id}_2" }
|
||||
|
||||
it 'displays and updates summary in sidebar', performs_jobs: true do
|
||||
before do
|
||||
click '.tabsSidebar-tab[data-tab=summary]'
|
||||
end
|
||||
|
||||
expect(page).to have_text 'CONVERSATION SUMMARY'
|
||||
expect(page).to have_text initial_summary
|
||||
it 'displays and updates summary in sidebar', performs_jobs: true do
|
||||
within '.sidebar[data-tab="summary"]' do
|
||||
expect(page).to have_text 'Conversation Summary'
|
||||
expect(page).to have_text initial_summary
|
||||
end
|
||||
|
||||
create(:ticket_article, ticket:)
|
||||
|
||||
@@ -52,7 +58,52 @@ RSpec.describe 'Ticket Summary', authenticated_as: :authenticate, type: :system
|
||||
|
||||
perform_enqueued_jobs(only: TicketAIAssistanceSummarizeJob)
|
||||
|
||||
expect(page).to have_text updated_summary
|
||||
within '.sidebar[data-tab="summary"]' do
|
||||
expect(page).to have_text updated_summary
|
||||
end
|
||||
end
|
||||
|
||||
context 'when suggestions are available' do
|
||||
let(:initial_suggestions) do
|
||||
[
|
||||
Faker::Lorem.unique.sentence,
|
||||
Faker::Lorem.unique.sentence,
|
||||
Faker::Lorem.unique.sentence,
|
||||
]
|
||||
end
|
||||
|
||||
it 'shows add buttons' do
|
||||
within '.sidebar[data-tab="summary"]' do
|
||||
expect(page).to have_text 'Suggested Next Steps'
|
||||
expect(find_all('button[aria-label="Add as checklist item"]').length).to eq(3)
|
||||
expect(page).to have_button 'Add all to checklist'
|
||||
|
||||
find_all('button[aria-label="Add as checklist item"]').first.click
|
||||
end
|
||||
|
||||
expect(page).to have_text 'Checklist item successfully added.'
|
||||
|
||||
within '.sidebar[data-tab="summary"]' do
|
||||
click_on 'Add all to checklist'
|
||||
end
|
||||
|
||||
within '.sidebar[data-tab="checklist"]' do
|
||||
expect(page).to have_text 'Checklist'
|
||||
expect(find_all('table tbody tr').length).to eq(4)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with checklist feature disabled' do
|
||||
let(:checklist) { false }
|
||||
|
||||
it 'does not show add buttons' do
|
||||
within '.sidebar[data-tab="summary"]' do
|
||||
expect(page).to have_text 'Suggested Next Steps'
|
||||
expect(page).to have_no_button 'Add'
|
||||
expect(page).to have_no_button 'Add all to checklist'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,7 +124,9 @@ RSpec.describe 'Ticket Summary', authenticated_as: :authenticate, type: :system
|
||||
|
||||
click_on('See Summary')
|
||||
|
||||
expect(page).to have_css('.js-headline', text: 'Summary')
|
||||
within '.sidebar[data-tab="summary"]' do
|
||||
expect(page).to have_text('Summary')
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows to hide Summary banner' do
|
||||
|
||||
Reference in New Issue
Block a user