mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
Feature: Desktop view - Support warning notes for creating articles
Co-authored-by: Benjamin Scharf <bs@zammad.com> Co-authored-by: Joe Schroecker <js@zammad.com>
This commit is contained in:
parent
439105bb1d
commit
68712c1006
11 changed files with 549 additions and 14 deletions
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
import { visitView } from '#tests/support/components/visitView.ts'
|
||||
import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
|
||||
import { mockPermissions } from '#tests/support/mock-permissions.ts'
|
||||
|
||||
describe('ticket create hint note', () => {
|
||||
beforeEach(() => {
|
||||
mockPermissions(['ticket.agent'])
|
||||
})
|
||||
|
||||
it('shows the article-type note when ui_ticket_create_notes is configured for the selected type', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_task_mananger_max_task_count: 30,
|
||||
ui_ticket_create_available_types: ['phone-in', 'phone-out', 'email-out'],
|
||||
ui_ticket_create_notes: {
|
||||
'phone-in': 'Please fill in the call details carefully.',
|
||||
},
|
||||
})
|
||||
|
||||
const view = await visitView('/ticket/create')
|
||||
|
||||
await view.findByRole('tab', { selected: true, name: 'Received call' })
|
||||
|
||||
expect(await view.findByText('Please fill in the call details carefully.')).toBeVisible()
|
||||
})
|
||||
|
||||
it('does not show a note when ui_ticket_create_notes has no entry for the selected type', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_task_mananger_max_task_count: 30,
|
||||
ui_ticket_create_available_types: ['phone-in', 'phone-out', 'email-out'],
|
||||
ui_ticket_create_notes: {
|
||||
'email-out': 'Email-only note.',
|
||||
},
|
||||
})
|
||||
|
||||
const view = await visitView('/ticket/create')
|
||||
|
||||
await view.findByRole('tab', { selected: true, name: 'Received call' })
|
||||
|
||||
expect(view.queryByText('Email-only note.')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('updates the article-type note when switching sender type', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_task_mananger_max_task_count: 30,
|
||||
ui_ticket_create_available_types: ['phone-in', 'phone-out', 'email-out'],
|
||||
ui_ticket_create_notes: {
|
||||
'phone-out': 'Note for outbound call.',
|
||||
},
|
||||
})
|
||||
|
||||
const view = await visitView('/ticket/create')
|
||||
|
||||
await view.findByRole('tab', { selected: true, name: 'Received call' })
|
||||
|
||||
// Default type (phone-in) has no note — nothing visible yet.
|
||||
expect(view.queryByText('Note for outbound call.')).not.toBeInTheDocument()
|
||||
|
||||
await view.events.click(await view.findByText('Outbound call'))
|
||||
|
||||
expect(await view.findByText('Note for outbound call.')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
import { getNode } from '@formkit/core'
|
||||
import { within } from '@testing-library/vue'
|
||||
|
||||
import { visitView } from '#tests/support/components/visitView.ts'
|
||||
import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
|
||||
|
||||
import {
|
||||
mockFormUpdaterQuery,
|
||||
waitForFormUpdaterQueryCalls,
|
||||
} from '#shared/components/Form/graphql/queries/formUpdater.mocks.ts'
|
||||
import { mockTicketQuery } from '#shared/entities/ticket/graphql/queries/ticket.mocks.ts'
|
||||
import { createDummyTicket } from '#shared/entities/ticket-article/__tests__/mocks/ticket.ts'
|
||||
|
||||
describe('article reply hint note', () => {
|
||||
const makeFormUpdaterOptions = () => ({
|
||||
formUpdater: {
|
||||
fields: {
|
||||
group_id: {
|
||||
options: [
|
||||
{ value: 1, label: 'Users' },
|
||||
{ value: 2, label: 'test group' },
|
||||
],
|
||||
},
|
||||
owner_id: { options: [{ value: 3, label: 'Test Admin Agent' }] },
|
||||
state_id: {
|
||||
options: [
|
||||
{ value: 4, label: 'closed' },
|
||||
{ value: 2, label: 'open' },
|
||||
{ value: 6, label: 'pending close' },
|
||||
{ value: 3, label: 'pending reminder' },
|
||||
],
|
||||
},
|
||||
pending_time: { show: false },
|
||||
priority_id: {
|
||||
options: [
|
||||
{ value: 1, label: '1 low' },
|
||||
{ value: 2, label: '2 normal' },
|
||||
{ value: 3, label: '3 high' },
|
||||
],
|
||||
},
|
||||
},
|
||||
flags: { newArticlePresent: false },
|
||||
},
|
||||
})
|
||||
|
||||
const openInternalNoteReply = async () => {
|
||||
mockTicketQuery({
|
||||
ticket: createDummyTicket({
|
||||
defaultPolicy: { update: true, agentReadAccess: true },
|
||||
}),
|
||||
})
|
||||
mockFormUpdaterQuery(makeFormUpdaterOptions())
|
||||
|
||||
const view = await visitView('/tickets/1')
|
||||
await waitForFormUpdaterQueryCalls()
|
||||
await view.events.click(await view.findByRole('button', { name: 'Add internal note' }))
|
||||
const complementary = await view.findByRole('complementary', { name: 'Reply' })
|
||||
await getNode('form-ticket-edit-1')?.settled
|
||||
return { view, complementary }
|
||||
}
|
||||
|
||||
it('shows the hint note when ui_ticket_add_article_hint is configured for the current article type and visibility', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_ticket_zoom_article_note_new_internal: true,
|
||||
ui_ticket_add_article_hint: {
|
||||
'note-internal': 'Please keep internal notes concise.',
|
||||
},
|
||||
})
|
||||
|
||||
const { complementary } = await openInternalNoteReply()
|
||||
|
||||
// Reply form defaults to Internal — hint should be visible.
|
||||
expect(
|
||||
await within(complementary).findByText('Please keep internal notes concise.'),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
it('does not show the hint note when ui_ticket_add_article_hint has no entry for the current type and visibility', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_ticket_zoom_article_note_new_internal: true,
|
||||
ui_ticket_add_article_hint: {
|
||||
'note-public': 'Public note hint.',
|
||||
},
|
||||
})
|
||||
|
||||
const { complementary } = await openInternalNoteReply()
|
||||
|
||||
expect(within(complementary).queryByText('Public note hint.')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('updates the hint note when visibility is changed', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_ticket_zoom_article_note_new_internal: true,
|
||||
ui_ticket_add_article_hint: {
|
||||
'note-public': 'Public note hint.',
|
||||
},
|
||||
})
|
||||
|
||||
const { view, complementary } = await openInternalNoteReply()
|
||||
|
||||
expect(within(complementary).queryByText('Public note hint.')).not.toBeInTheDocument()
|
||||
|
||||
await view.events.click(within(complementary).getByLabelText('Visibility'))
|
||||
await view.events.click(await view.findByRole('option', { name: 'Public' }))
|
||||
await getNode('form-ticket-edit-1')?.settled
|
||||
|
||||
expect(await within(complementary).findByText('Public note hint.')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
@ -145,7 +145,18 @@ const formSchema = defineFormSchema([
|
|||
props: {
|
||||
variant: 'warning',
|
||||
},
|
||||
children: '$t($getAdditionalCreateNote($values.articleSenderType))',
|
||||
children: [
|
||||
{
|
||||
isLayout: true,
|
||||
element: 'div',
|
||||
attrs: {
|
||||
// We convert light weight markup
|
||||
// The input is not sanitized and relies on the administrator to provide safe links
|
||||
innerHTML: '$markup($t($getAdditionalCreateNote($values.articleSenderType)))',
|
||||
},
|
||||
children: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
if: '$values.ticket_duplicate_detection.count > 0',
|
||||
|
|
@ -277,7 +288,7 @@ const schemaData = reactive({
|
|||
getTabLabel: (value: string) => `tab-label-${value}`,
|
||||
getTabPanelId: (value: string) => `tab-panel-${value}`,
|
||||
existingAdditionalCreateNotes: () => {
|
||||
return Object.keys(additionalCreateNotes).length > 0
|
||||
return Object.keys(additionalCreateNotes.value).length > 0
|
||||
},
|
||||
getAdditionalCreateNote: (value: string) => {
|
||||
return additionalCreateNotes.value[value]
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ const {
|
|||
ticketSchema,
|
||||
articleSchema,
|
||||
currentArticleType,
|
||||
currentSchemaArticleType,
|
||||
ticketArticleTypes,
|
||||
ticketArticleDefaultValues,
|
||||
securityIntegration,
|
||||
|
|
@ -214,6 +215,7 @@ const {
|
|||
isTicketEditable,
|
||||
articleTypeHandler,
|
||||
articleTypeSelectHandler,
|
||||
additionalAddArticleNotes,
|
||||
} = useTicketEditForm(ticket, form)
|
||||
|
||||
const { signatureHandling } = useTicketSignature('email')
|
||||
|
|
@ -298,7 +300,17 @@ const ticketEditSchemaData = reactive({
|
|||
formArticleReplyLocation,
|
||||
securityIntegration,
|
||||
newTicketArticlePresent,
|
||||
currentArticleType,
|
||||
currentArticleType: currentSchemaArticleType,
|
||||
existingAdditionalAddArticleNotes: () => {
|
||||
return Object.keys(additionalAddArticleNotes.value).length > 0
|
||||
},
|
||||
getAdditionalAddArticleNote: (articleType?: AppSpecificTicketArticleType) => {
|
||||
if (!articleType) return undefined
|
||||
|
||||
const accessor = `${articleType.value}-${articleType.internal ? 'internal' : 'public'}`
|
||||
|
||||
return additionalAddArticleNotes.value[accessor]
|
||||
},
|
||||
})
|
||||
|
||||
const ticketEditSchema = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
import { getNode } from '@formkit/core'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
|
||||
import type { ExtendedRenderResult } from '#tests/support/components/index.ts'
|
||||
import { visitView } from '#tests/support/components/visitView.ts'
|
||||
import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
|
||||
import { mockPermissions } from '#tests/support/mock-permissions.ts'
|
||||
|
||||
import '#tests/graphql/builders/mocks.ts'
|
||||
import {
|
||||
mockFormUpdaterQuery,
|
||||
waitForFormUpdaterQueryCalls,
|
||||
} from '#shared/components/Form/graphql/queries/formUpdater.mocks.ts'
|
||||
|
||||
describe('ticket create hint note', () => {
|
||||
beforeEach(() => {
|
||||
mockPermissions(['ticket.agent'])
|
||||
})
|
||||
|
||||
const nextStep = async (view: ExtendedRenderResult) => {
|
||||
await view.events.click(view.getByRole('button', { name: 'Continue' }))
|
||||
}
|
||||
|
||||
const visitTicketCreate = async (path = '/tickets/create') => {
|
||||
mockFormUpdaterQuery({
|
||||
formUpdater: {
|
||||
fields: {
|
||||
group_id: {
|
||||
show: true,
|
||||
options: [
|
||||
{
|
||||
label: 'Users',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
clearable: true,
|
||||
},
|
||||
owner_id: {
|
||||
show: true,
|
||||
options: [{ value: 100, label: 'Max Mustermann' }],
|
||||
},
|
||||
priority_id: {
|
||||
show: true,
|
||||
options: [
|
||||
{ value: 1, label: '1 low' },
|
||||
{ value: 2, label: '2 normal' },
|
||||
{ value: 3, label: '3 high' },
|
||||
],
|
||||
clearable: true,
|
||||
},
|
||||
pending_time: {
|
||||
show: false,
|
||||
required: false,
|
||||
hidden: false,
|
||||
disabled: false,
|
||||
},
|
||||
state_id: {
|
||||
show: true,
|
||||
options: [
|
||||
{ value: 4, label: 'closed' },
|
||||
{ value: 2, label: 'open' },
|
||||
{ value: 7, label: 'pending close' },
|
||||
{ value: 3, label: 'pending reminder' },
|
||||
],
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const view = await visitView(path, { mockApollo: false })
|
||||
|
||||
await flushPromises()
|
||||
await getNode('ticket-create')?.settled
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
it('shows the article-type note when ui_ticket_create_notes is configured for the selected type', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_task_mananger_max_task_count: 30,
|
||||
ui_ticket_create_available_types: ['phone-in', 'phone-out', 'email-out'],
|
||||
ui_ticket_create_notes: {
|
||||
'phone-in': 'Please fill in the call details carefully.',
|
||||
},
|
||||
})
|
||||
|
||||
const view = await visitTicketCreate()
|
||||
await flushPromises()
|
||||
await getNode('ticket-create')?.settled
|
||||
|
||||
await view.events.type(await view.findByLabelText('Title'), 'Foobar')
|
||||
await waitForFormUpdaterQueryCalls()
|
||||
await nextStep(view)
|
||||
|
||||
expect(await view.findByText('Please fill in the call details carefully.')).toBeVisible()
|
||||
})
|
||||
|
||||
it('does not show a note when ui_ticket_create_notes has no entry for the selected type', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_task_mananger_max_task_count: 30,
|
||||
ui_ticket_create_available_types: ['phone-in', 'phone-out', 'email-out'],
|
||||
ui_ticket_create_notes: {
|
||||
'email-out': 'Email-only note.',
|
||||
},
|
||||
})
|
||||
|
||||
const view = await visitTicketCreate()
|
||||
|
||||
await view.events.click(await view.findByText('Received call'))
|
||||
|
||||
expect(view.queryByText('Email-only note.')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('updates the article-type note when switching sender type', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_task_mananger_max_task_count: 30,
|
||||
ui_ticket_create_available_types: ['phone-in', 'phone-out', 'email-out'],
|
||||
ui_ticket_create_notes: {
|
||||
'phone-out': 'Note for outbound call.',
|
||||
},
|
||||
})
|
||||
|
||||
const view = await visitTicketCreate()
|
||||
|
||||
await view.findByText('Received call')
|
||||
|
||||
// Default type (phone-in) has no note — nothing visible yet.
|
||||
expect(view.queryByText('Note for outbound call.')).not.toBeInTheDocument()
|
||||
|
||||
await view.events.click(await view.findByText('Outbound call'))
|
||||
|
||||
expect(await view.findByText('Note for outbound call.')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
import { getNode } from '@formkit/core'
|
||||
|
||||
import { visitView } from '#tests/support/components/visitView.ts'
|
||||
import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
|
||||
import { mockPermissions } from '#tests/support/mock-permissions.ts'
|
||||
|
||||
import {
|
||||
mockFormUpdaterQuery,
|
||||
waitForFormUpdaterQueryCalls,
|
||||
} from '#shared/components/Form/graphql/queries/formUpdater.mocks.ts'
|
||||
import { mockTicketQuery } from '#shared/entities/ticket/graphql/queries/ticket.mocks.ts'
|
||||
import { createDummyTicket } from '#shared/entities/ticket-article/__tests__/mocks/ticket.ts'
|
||||
|
||||
import { clearTicketArticlesLoadedState } from '../composable/useTicketArticlesVariables.ts'
|
||||
|
||||
import '#tests/graphql/builders/mocks.ts'
|
||||
|
||||
describe('article reply hint note', () => {
|
||||
beforeEach(() => {
|
||||
mockPermissions(['ticket.agent'])
|
||||
clearTicketArticlesLoadedState()
|
||||
})
|
||||
const makeFormUpdaterOptions = () => ({
|
||||
formUpdater: {
|
||||
fields: {
|
||||
group_id: {
|
||||
options: [
|
||||
{ value: 1, label: 'Users' },
|
||||
{ value: 2, label: 'test group' },
|
||||
],
|
||||
},
|
||||
owner_id: { options: [{ value: 3, label: 'Test Admin Agent' }] },
|
||||
state_id: {
|
||||
options: [
|
||||
{ value: 4, label: 'closed' },
|
||||
{ value: 2, label: 'open' },
|
||||
{ value: 6, label: 'pending close' },
|
||||
{ value: 3, label: 'pending reminder' },
|
||||
],
|
||||
},
|
||||
pending_time: { show: false },
|
||||
priority_id: {
|
||||
options: [
|
||||
{ value: 1, label: '1 low' },
|
||||
{ value: 2, label: '2 normal' },
|
||||
{ value: 3, label: '3 high' },
|
||||
],
|
||||
},
|
||||
},
|
||||
flags: { newArticlePresent: false },
|
||||
},
|
||||
})
|
||||
|
||||
const openInternalNoteReply = async () => {
|
||||
mockTicketQuery({
|
||||
ticket: createDummyTicket({
|
||||
defaultPolicy: { update: true, agentReadAccess: true },
|
||||
}),
|
||||
})
|
||||
mockFormUpdaterQuery(makeFormUpdaterOptions())
|
||||
|
||||
const view = await visitView('/tickets/1', { mockApollo: false })
|
||||
await waitForFormUpdaterQueryCalls()
|
||||
await getNode('form-ticket-edit-1')?.settled
|
||||
|
||||
await view.events.click(view.getByRole('button', { name: 'Add reply' }))
|
||||
return { view }
|
||||
}
|
||||
|
||||
it('shows the hint note when ui_ticket_add_article_hint is configured for the current article type and visibility', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_ticket_zoom_article_note_new_internal: true,
|
||||
ui_ticket_add_article_hint: {
|
||||
'note-internal': 'Please keep internal notes concise.',
|
||||
'note-public': 'Attention public note.',
|
||||
},
|
||||
})
|
||||
|
||||
const { view } = await openInternalNoteReply()
|
||||
|
||||
// Reply form defaults to Internal — hint should be visible.
|
||||
await view.events.click(await view.findByText('Note'))
|
||||
await view.findByRole('option', { selected: true, name: 'Note' })
|
||||
expect(await view.findByText('Please keep internal notes concise.')).toBeVisible()
|
||||
})
|
||||
|
||||
it('does not show the hint note when ui_ticket_add_article_hint has no entry for the current type and visibility', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_ticket_zoom_article_note_new_internal: true,
|
||||
ui_ticket_add_article_hint: {
|
||||
'note-public': 'Public note hint.',
|
||||
},
|
||||
})
|
||||
|
||||
const { view } = await openInternalNoteReply()
|
||||
|
||||
// Reply form defaults to Internal — hint should be visible.
|
||||
await view.events.click(await view.findByText('Note'))
|
||||
await view.findByRole('option', { selected: true, name: 'Note' })
|
||||
await view.events.click(await view.findByText('Internal'))
|
||||
await view.events.click(await view.findByRole('option', { selected: true, name: 'Internal' }))
|
||||
expect(view.queryByText('Public note hint.')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('updates the hint note when visibility is changed', async () => {
|
||||
mockApplicationConfig({
|
||||
ui_ticket_zoom_article_note_new_internal: true,
|
||||
ui_ticket_add_article_hint: {
|
||||
'note-public': 'Public note hint.',
|
||||
},
|
||||
})
|
||||
|
||||
const { view } = await openInternalNoteReply()
|
||||
|
||||
expect(view.queryByText('Public note hint.')).not.toBeInTheDocument()
|
||||
|
||||
await view.events.click(view.getByLabelText('Visibility'))
|
||||
await view.events.click(await view.findByRole('option', { selected: false, name: 'Public' }))
|
||||
|
||||
await getNode('form-ticket-edit-1')?.settled
|
||||
|
||||
expect(await view.findByText('Public note hint.')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
@ -153,11 +153,22 @@ const ticketArticleTypeSection = getFormSchemaGroupSection(
|
|||
{
|
||||
if: '$existingAdditionalCreateNotes() && $getAdditionalCreateNote($values.articleSenderType) !== undefined',
|
||||
isLayout: true,
|
||||
element: 'p',
|
||||
attrs: {
|
||||
class: 'my-10 text-base text-center text-yellow',
|
||||
component: 'CommonAlert',
|
||||
props: {
|
||||
variant: 'warning',
|
||||
},
|
||||
children: '$getAdditionalCreateNote($values.articleSenderType)',
|
||||
children: [
|
||||
{
|
||||
isLayout: true,
|
||||
element: 'div',
|
||||
attrs: {
|
||||
// We convert light weight markup
|
||||
// The input is not sanitized and relies on the administrator to provide safe content
|
||||
innerHTML: '$markup($t($getAdditionalCreateNote($values.articleSenderType)))',
|
||||
},
|
||||
children: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
true,
|
||||
|
|
@ -352,10 +363,10 @@ const schemaData = reactive({
|
|||
allSteps,
|
||||
securityIntegration,
|
||||
existingAdditionalCreateNotes: () => {
|
||||
return Object.keys(additionalCreateNotes).length > 0
|
||||
return Object.keys(additionalCreateNotes.value).length > 0
|
||||
},
|
||||
getAdditionalCreateNote: (value: string) => {
|
||||
return i18n.t(additionalCreateNotes.value[value])
|
||||
return additionalCreateNotes.value[value]
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { useTicketEditForm } from '#shared/entities/ticket/composables/useTicket
|
|||
import { useTicketView } from '#shared/entities/ticket/composables/useTicketView.ts'
|
||||
import { TicketUpdatesDocument } from '#shared/entities/ticket/graphql/subscriptions/ticketUpdates.api.ts'
|
||||
import type { TicketUpdateFormData } from '#shared/entities/ticket/types.ts'
|
||||
import type { AppSpecificTicketArticleType } from '#shared/entities/ticket-article/action/plugins/types.ts'
|
||||
import { useErrorHandler } from '#shared/errors/useErrorHandler.ts'
|
||||
import UserError from '#shared/errors/UserError.ts'
|
||||
import type {
|
||||
|
|
@ -93,10 +94,12 @@ const {
|
|||
currentArticleType,
|
||||
ticketSchema,
|
||||
articleSchema,
|
||||
currentSchemaArticleType,
|
||||
securityIntegration,
|
||||
isTicketEditable,
|
||||
articleTypeHandler,
|
||||
articleTypeSelectHandler,
|
||||
additionalAddArticleNotes,
|
||||
} = useTicketEditForm(ticket, form)
|
||||
|
||||
const needSpaceForSaveBanner = computed(() => isTicketEditable.value && isDirty.value)
|
||||
|
|
@ -283,7 +286,17 @@ const ticketEditSchemaData = reactive({
|
|||
securityIntegration,
|
||||
newTicketArticleRequested,
|
||||
newTicketArticlePresent,
|
||||
currentArticleType,
|
||||
currentArticleType: currentSchemaArticleType,
|
||||
existingAdditionalAddArticleNotes: () => {
|
||||
return Object.keys(additionalAddArticleNotes.value).length > 0
|
||||
},
|
||||
getAdditionalAddArticleNote: (articleType?: AppSpecificTicketArticleType) => {
|
||||
if (!articleType) return undefined
|
||||
|
||||
const accessor = `${articleType.value}-${articleType.internal ? 'internal' : 'public'}`
|
||||
|
||||
return additionalAddArticleNotes.value[accessor]
|
||||
},
|
||||
})
|
||||
|
||||
const { isOpened: commonSelectOpened } = useCommonSelect()
|
||||
|
|
|
|||
|
|
@ -41,6 +41,22 @@ export const useTicketEditForm = (
|
|||
|
||||
const currentArticleType = shallowRef<AppSpecificTicketArticleType>()
|
||||
|
||||
const hasInternalArticle = computed(
|
||||
() =>
|
||||
(form.value?.values?.article as { internal?: boolean })?.internal ??
|
||||
currentArticleType.value?.internal ??
|
||||
false,
|
||||
)
|
||||
|
||||
const currentSchemaArticleType = computed(() => {
|
||||
if (!currentArticleType.value) return undefined
|
||||
|
||||
return {
|
||||
...currentArticleType.value,
|
||||
internal: hasInternalArticle.value,
|
||||
}
|
||||
})
|
||||
|
||||
const recipientContact = computed(() => currentArticleType.value?.options?.recipientContact)
|
||||
const editorType = computed(() => currentArticleType.value?.contentType)
|
||||
|
||||
|
|
@ -88,6 +104,12 @@ export const useTicketEditForm = (
|
|||
|
||||
const isMobileApp = appName === 'mobile'
|
||||
|
||||
const application = useApplicationStore()
|
||||
|
||||
const additionalAddArticleNotes = computed(
|
||||
() => (application.config.ui_ticket_add_article_hint as Record<string, string>) || {},
|
||||
)
|
||||
|
||||
const ticketSchema = {
|
||||
type: 'group',
|
||||
name: 'ticket', // will be flattened in the form submit result
|
||||
|
|
@ -125,6 +147,27 @@ export const useTicketEditForm = (
|
|||
name: 'article',
|
||||
isGroupOrList: true,
|
||||
children: [
|
||||
{
|
||||
if: '$existingAdditionalAddArticleNotes() && $getAdditionalAddArticleNote($currentArticleType) !== undefined',
|
||||
isLayout: true,
|
||||
component: 'CommonAlert',
|
||||
props: {
|
||||
variant: 'warning',
|
||||
class: 'col-span-2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
isLayout: true,
|
||||
element: 'div',
|
||||
attrs: {
|
||||
// We convert light weight markup
|
||||
// The input is not sanitized and relies on the administrator to provide safe content
|
||||
innerHTML: '$markup($t($getAdditionalAddArticleNote($currentArticleType)))',
|
||||
},
|
||||
children: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'hidden',
|
||||
name: 'inReplyTo',
|
||||
|
|
@ -332,8 +375,6 @@ export const useTicketEditForm = (
|
|||
})
|
||||
}
|
||||
|
||||
const application = useApplicationStore()
|
||||
|
||||
const securityIntegration = computed<boolean>(
|
||||
() => (application.config.smime_integration || application.config.pgp_integration) ?? false,
|
||||
)
|
||||
|
|
@ -342,6 +383,7 @@ export const useTicketEditForm = (
|
|||
ticketSchema,
|
||||
articleSchema,
|
||||
currentArticleType,
|
||||
currentSchemaArticleType,
|
||||
ticketArticleTypes,
|
||||
ticketArticleDefaultValues,
|
||||
securityIntegration,
|
||||
|
|
@ -350,5 +392,6 @@ export const useTicketEditForm = (
|
|||
isTicketEditable,
|
||||
articleTypeHandler: articleTypeChangeHandler,
|
||||
articleTypeSelectHandler,
|
||||
additionalAddArticleNotes,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ describe('markup()', () => {
|
|||
expect(markup('_underline_')).toBe('<u>underline</u>')
|
||||
expect(markup('//strikethrough//')).toBe('<del>strikethrough</del>')
|
||||
expect(markup('§keyboard§')).toBe('<kbd>keyboard</kbd>')
|
||||
expect(markup('Paragraph¶New line')).toBe('Paragraph<br>New line')
|
||||
expect(markup('[link](https://zammad.org)')).toBe(
|
||||
'<a href="https://zammad.org" target="_blank">link</a>',
|
||||
'<a href="https://zammad.org" target="_blank" rel="noopener noreferrer">link</a>',
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -26,6 +27,7 @@ describe('cleanupMarkup()', () => {
|
|||
expect(cleanupMarkup('_underline_')).toBe('underline')
|
||||
expect(cleanupMarkup('//strikethrough//')).toBe('strikethrough')
|
||||
expect(cleanupMarkup('§keyboard§')).toBe('keyboard')
|
||||
expect(cleanupMarkup('Paragraph¶New line')).toBe('ParagraphNew line')
|
||||
expect(cleanupMarkup('[link](https://zammad.org)')).toBe('link')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ export const markup = (source: string): string =>
|
|||
.replace(/_(.+?)_/gm, '<u>$1</u>')
|
||||
.replace(/\/\/(.+?)\/\//gm, '<del>$1</del>')
|
||||
.replace(/§(.+?)§/gm, '<kbd>$1</kbd>')
|
||||
.replace(/\[(.+?)\]\((.+?)\)/gm, '<a href="$2" target="_blank">$1</a>')
|
||||
.replace(/¶/gm, '<br>')
|
||||
.replace(
|
||||
/\[(.+?)\]\((.+?)\)/gm,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||
)
|
||||
|
||||
export const cleanupMarkup = (source: string): string =>
|
||||
source
|
||||
|
|
@ -19,6 +23,7 @@ export const cleanupMarkup = (source: string): string =>
|
|||
.replace(/_(.+?)_/gm, '$1')
|
||||
.replace(/\/\/(.+?)\/\//gm, '$1')
|
||||
.replace(/§(.+?)§/gm, '$1')
|
||||
.replace(/¶/gm, '')
|
||||
.replace(/\[(.+?)\]\((.+?)\)/gm, '$1')
|
||||
|
||||
export const normalizeImageSizingInHtml = (html: string) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue