Browse Source

Feature: Desktop View - Right sidebar integrations I-doit

Co-authored-by: Benjamin Scharf <bs@zammad.com>
Co-authored-by: Martin Gruner <mg@zammad.com>
Benjamin Scharf 3 months ago
parent
commit
8e6a6a48f2

+ 1 - 0
app/frontend/apps/desktop/components/CommonCalendarPreviewFlyout/__tests__/CommonCalendarPreviewFlyout.spec.ts

@@ -39,6 +39,7 @@ const renderCommonCalendarPreviewFlyout = async (
     },
     ...options,
     router: true,
+    form: true,
     global: {
       stubs: {
         teleport: true,

+ 156 - 55
app/frontend/apps/desktop/components/CommonSimpleTable/CommonSimpleTable.vue

@@ -1,9 +1,14 @@
 <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
+import { computed, toRef } from 'vue'
+
+import type { Props as CommonLinkProps } from '#shared/components/CommonLink/CommonLink.vue'
+
 import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
 import type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
 import SimpleTableRow from '#desktop/components/CommonSimpleTable/SimpleTableRow.vue'
+import { useTableCheckboxes } from '#desktop/components/CommonSimpleTable/useTableCheckboxes.ts'
 
 import type { TableHeader, TableItem } from './types.ts'
 
@@ -12,7 +17,12 @@ export interface Props {
   items: TableItem[]
   actions?: MenuItem[]
   onClickRow?: (tableItem: TableItem) => void
+  /**
+   * Used to set a default selected row
+   * Is not used for checkbox
+   * */
   selectedRowId?: string
+  hasCheckboxColumn?: boolean
 }
 
 const props = defineProps<Props>()
@@ -39,6 +49,19 @@ const cellAlignmentClasses = {
   left: 'text-left',
 }
 
+const tableHeaders = computed(() =>
+  props.hasCheckboxColumn
+    ? [
+        {
+          key: 'checkbox',
+          label: __('Select all entries'),
+          columnClass: 'w-10',
+        } as TableHeader,
+        ...props.headers,
+      ]
+    : props.headers,
+)
+
 const columnSeparatorClasses =
   'border-r border-neutral-100 dark:border-gray-900'
 
@@ -46,16 +69,35 @@ const getTooltipText = (item: TableItem, header: TableHeader) => {
   return header.truncate ? item[header.key] : undefined
 }
 
-defineExpose({
-  getTooltipText,
+const checkedRows = defineModel<Array<TableItem>>('checkedRows', {
+  required: false,
+  default: (props: Props) => props.items.filter((item) => item.checked), // is not reactive by default and making it reactive causes other issues.
 })
+
+const {
+  hasCheckboxId,
+  allCheckboxRowsSelected,
+  selectAllRowCheckboxes,
+  handleCheckboxUpdate,
+} = useTableCheckboxes(checkedRows, toRef(props, 'items'))
+
+const rowHandlers = computed(() =>
+  props.onClickRow || props.hasCheckboxColumn
+    ? {
+        'click-row': (event: TableItem) => {
+          if (props.onClickRow) props.onClickRow(event)
+          if (props.hasCheckboxColumn) handleCheckboxUpdate(event)
+        },
+      }
+    : {},
+)
 </script>
 
 <template>
   <table class="pb-3">
     <thead>
       <th
-        v-for="header in headers"
+        v-for="header in tableHeaders"
         :key="header.key"
         class="h-10 p-2.5 text-xs ltr:text-left rtl:text-right"
         :class="[
@@ -63,18 +105,33 @@ defineExpose({
           header.columnSeparator && columnSeparatorClasses,
         ]"
       >
-        <slot :name="`column-header-${header.key}`" :header="header">
-          <CommonLabel
-            class="-:font-normal -:text-stone-200 -:dark:text-neutral-500"
-            :class="[
-              cellAlignmentClasses[header.alignContent || 'left'],
-              header.labelClass || '',
-            ]"
-            size="small"
-          >
-            {{ $t(header.label, ...(header.labelPlaceholder || [])) }}
-          </CommonLabel>
-        </slot>
+        <FormKit
+          v-if="hasCheckboxColumn && header.key === 'checkbox'"
+          name="checkbox-all-rows"
+          :aria-label="
+            allCheckboxRowsSelected
+              ? $t('Deselect all entries')
+              : $t('Select all entries')
+          "
+          type="checkbox"
+          :model-value="allCheckboxRowsSelected"
+          @update:model-value="selectAllRowCheckboxes"
+        />
+
+        <template v-else>
+          <slot :name="`column-header-${header.key}`" :header="header">
+            <CommonLabel
+              class="-:font-normal -:text-stone-200 -:dark:text-neutral-500"
+              :class="[
+                cellAlignmentClasses[header.alignContent || 'left'],
+                header.labelClass || '',
+              ]"
+              size="small"
+            >
+              {{ $t(header.label, ...(header.labelPlaceholder || [])) }}
+            </CommonLabel>
+          </slot>
+        </template>
 
         <slot :name="`header-suffix-${header.key}`" :item="header" />
       </th>
@@ -91,12 +148,13 @@ defineExpose({
         v-for="item in items"
         :key="item.id"
         :item="item"
-        :is-row-selected="item.id === props.selectedRowId"
-        @click-row="onClickRow"
+        :is-row-selected="!hasCheckboxColumn && item.id === props.selectedRowId"
+        :has-checkbox="hasCheckboxColumn"
+        v-on="rowHandlers"
       >
         <template #default="{ isRowSelected }">
           <td
-            v-for="header in headers"
+            v-for="header in tableHeaders"
             :key="`${item.id}-${header.key}`"
             class="h-10 p-2.5 text-sm"
             :class="[
@@ -107,46 +165,89 @@ defineExpose({
               },
             ]"
           >
-            <slot
-              :name="`column-cell-${header.key}`"
-              :item="item"
-              :is-row-selected="isRowSelected"
-              :header="header"
-            >
-              <CommonLabel
-                v-tooltip.truncate="getTooltipText(item, header)"
-                class="-:text-gray-100 -:dark:text-neutral-400 inline group-hover:text-black group-active:text-black group-hover:dark:text-white group-active:dark:text-white"
-                :class="[
-                  {
-                    'text-black dark:text-white': isRowSelected,
-                  },
-                ]"
+            <FormKit
+              v-if="hasCheckboxColumn && header.key === 'checkbox'"
+              :key="`checkbox-${item.id}-${header.key}`"
+              :name="`checkbox-${item.id}`"
+              :aria-label="
+                hasCheckboxId(item.id)
+                  ? $t('Deselect this entry')
+                  : $t('Select this entry')
+              "
+              type="checkbox"
+              alternative-backrgound
+              :classes="{
+                decorator:
+                  'group-active:formkit-checked:border-white group-hover:dark:border-white group-hover:group-active:border-white group-hover:group-active:peer-hover:border-white group-hover:formkit-checked:border-black group-hover:dark:formkit-checked:border-white group-hover:dark:peer-hover:border-white  ltr:group-hover:dark:group-hover:peer-hover:formkit-checked:border-white ltr:group-hover:peer-hover:dark:border-white rtl:group-hover:peer-hover:dark:border-white ltr:group-hover:peer-hover:border-black rtl:group-hover:peer-hover:border-black  group-hover:border-black',
+                decoratorIcon:
+                  'group-active:formkit-checked:text-white group-hover:formkit-checked:text-black group-hover:formkit-checked:dark:text-white',
+              }"
+              :disabled="!!item.disabled"
+              :model-value="hasCheckboxId(item.id)"
+              @click="handleCheckboxUpdate(item)"
+              @keydown.enter="handleCheckboxUpdate(item)"
+              @keydown.space="handleCheckboxUpdate(item)"
+            />
+            <template v-else>
+              <slot
+                :name="`column-cell-${header.key}`"
+                :item="item"
+                :is-row-selected="isRowSelected"
+                :header="header"
               >
-                <template v-if="!item[header.key]">-</template>
-                <template v-else-if="header.type === 'timestamp_absolute'">
-                  <CommonDateTime
-                    :class="{
-                      'text-black dark:text-white': isRowSelected,
-                    }"
-                    :date-time="item[header.key] as string"
-                    type="absolute"
-                  />
-                </template>
-                <template v-else-if="header.type === 'timestamp'">
-                  <CommonDateTime
-                    :class="{
+                <CommonLink
+                  v-if="header.type === 'link'"
+                  v-tooltip.truncate="getTooltipText(item, header)"
+                  v-bind="item[header.key] as CommonLinkProps"
+                  :class="{
+                    'ltr:text-black rtl:text-black dark:text-white':
+                      isRowSelected,
+                  }"
+                  class="truncate text-sm hover:no-underline group-hover:text-black group-focus-visible:text-white group-active:text-white group-hover:dark:text-white"
+                  @click.stop
+                  @keydown.stop
+                  >{{ (item[header.key] as MenuItem).label }}
+                </CommonLink>
+                <CommonLabel
+                  v-else
+                  v-tooltip.truncate="getTooltipText(item, header)"
+                  class="-:text-gray-100 -:dark:text-neutral-400 inline group-hover:text-black group-focus-visible:text-white group-active:text-white group-hover:dark:text-white"
+                  :class="[
+                    {
                       'text-black dark:text-white': isRowSelected,
-                    }"
-                    :date-time="item[header.key] as string"
-                  />
-                </template>
-                <template v-else>
-                  {{ item[header.key] }}
-                </template>
-              </CommonLabel>
-            </slot>
+                    },
+                  ]"
+                >
+                  <template v-if="!item[header.key]">-</template>
+                  <template v-else-if="header.type === 'timestamp_absolute'">
+                    <CommonDateTime
+                      class="group-focus-visible:text-white"
+                      :class="{
+                        'text-black dark:text-white': isRowSelected,
+                      }"
+                      :date-time="item[header.key] as string"
+                      type="absolute"
+                    />
+                  </template>
+                  <template v-else-if="header.type === 'timestamp'">
+                    <CommonDateTime
+                      class="group-focus-visible:text-white"
+                      :class="{
+                        'text-black dark:text-white': isRowSelected,
+                      }"
+                      :date-time="item[header.key] as string"
+                      as
+                      string
+                    />
+                  </template>
+                  <template v-else>
+                    {{ item[header.key] }}
+                  </template>
+                </CommonLabel>
+              </slot>
 
-            <slot :name="`item-suffix-${header.key}`" :item="item" />
+              <slot :name="`item-suffix-${header.key}`" :item="item" />
+            </template>
           </td>
           <td v-if="actions" class="h-10 p-2.5 text-center">
             <slot name="actions" v-bind="{ actions, item }">

+ 13 - 9
app/frontend/apps/desktop/components/CommonSimpleTable/SimpleTableRow.vue

@@ -9,6 +9,7 @@ export interface Props {
   item: TableItem
   onClickRow?: (tableItem: TableItem) => void
   isRowSelected?: boolean
+  hasCheckbox?: boolean
 }
 
 const props = defineProps<Props>()
@@ -17,33 +18,36 @@ const emit = defineEmits<{
   'click-row': [TableItem]
 }>()
 
-const rowEventHandler = computed(() =>
-  props.onClickRow
+const rowEventHandler = computed(() => {
+  return (props.onClickRow || props.hasCheckbox) && !props.item.disabled
     ? {
         attrs: {
-          role: 'button',
-          tabindex: 0,
+          role: props.hasCheckbox ? undefined : 'button',
+          tabindex: props.hasCheckbox ? -1 : 0,
           ariaLabel: __('Select table row'),
           class:
-            'group focus-visible:outline-1 focus-visible:outline focus-visible:rounded-md active:bg-blue-800 active:dark:bg-blue-800 focus-visible:outline-blue-800 hover:bg-blue-600 dark:hover:bg-blue-900',
+            'group focus-visible:outline-transparent cursor-pointer active:bg-blue-800 active:dark:bg-blue-800 focus-visible:bg-blue-800 focus-visible:dark:bg-blue-900 focus-within:text-white hover:bg-blue-600 dark:hover:bg-blue-900',
         },
         events: {
-          click: () => emit('click-row', props.item),
+          click: () => {
+            ;(document.activeElement as HTMLElement)?.blur()
+            emit('click-row', props.item)
+          },
           keydown: (event: KeyboardEvent) => {
             if (event.key !== 'Enter') return
             emit('click-row', props.item)
           },
         },
       }
-    : { attrs: {}, events: {} },
-)
+    : { attrs: {}, events: {} }
+})
 </script>
 
 <template>
   <tr
     class="odd:bg-blue-200 odd:dark:bg-gray-700"
     :class="{
-      '!bg-blue-800': isRowSelected,
+      '!bg-blue-800': !hasCheckbox && isRowSelected,
     }"
     style="clip-path: xywh(0 0 100% 100% round 0.375rem)"
     data-test-id="simple-table-row"

+ 149 - 2
app/frontend/apps/desktop/components/CommonSimpleTable/__tests__/CommonSimpleTable.spec.ts

@@ -2,8 +2,12 @@
 
 import { waitFor, within } from '@testing-library/vue'
 import { vi } from 'vitest'
+import { ref } from 'vue'
 
-import { renderComponent } from '#tests/support/components/index.ts'
+import {
+  type ExtendedMountingOptions,
+  renderComponent,
+} from '#tests/support/components/index.ts'
 
 import { i18n } from '#shared/i18n.ts'
 
@@ -43,7 +47,10 @@ const tableActions: MenuItem[] = [
   },
 ]
 
-const renderTable = (props: Props, options = {}) => {
+const renderTable = (
+  props: Props,
+  options: ExtendedMountingOptions<Props> = { form: true },
+) => {
   return renderComponent(CommonSimpleTable, {
     ...options,
     props,
@@ -294,4 +301,144 @@ describe('CommonSimpleTable', () => {
       'text-red-500 font-bold',
     )
   })
+
+  it('supports adding a link to a cell', () => {
+    const wrapper = renderTable(
+      {
+        headers: [
+          {
+            key: 'urlTest',
+            label: 'Link Row',
+            type: 'link',
+          },
+        ],
+        items: [
+          {
+            id: 1,
+            urlTest: {
+              label: 'Example',
+              link: 'https://example.com',
+              openInNewTab: true,
+              external: true,
+            },
+          },
+        ],
+      },
+      { router: true },
+    )
+
+    const linkCell = wrapper.getByRole('link')
+
+    expect(linkCell).toHaveTextContent('Example')
+    expect(linkCell).toHaveAttribute('href', 'https://example.com')
+    expect(linkCell).toHaveAttribute('target', '_blank')
+  })
+
+  it('adds hover a')
+
+  it('supports row selection', async () => {
+    const checkedRows = ref([])
+
+    const items = [
+      {
+        id: 1,
+        label: 'selection data 1',
+      },
+      {
+        id: 2,
+        label: 'selection data 2',
+      },
+    ]
+
+    const wrapper = renderTable(
+      {
+        headers: [
+          {
+            key: 'urlTest',
+            label: 'Link Row',
+          },
+        ],
+        items,
+        hasCheckboxColumn: true,
+      },
+      { form: true, vModel: { checkedRows } },
+    )
+
+    expect(wrapper.getAllByRole('checkbox')).toHaveLength(3)
+
+    const selectAllCheckbox = wrapper.getByLabelText('Select all entries')
+
+    expect(selectAllCheckbox).not.toHaveAttribute('checked')
+
+    const rowCheckboxes = wrapper.getAllByRole('checkbox', {
+      name: 'Select this entry',
+    })
+
+    await wrapper.events.click(rowCheckboxes[0])
+    expect(rowCheckboxes[0]).toHaveAttribute('checked')
+
+    await wrapper.events.click(rowCheckboxes[1])
+
+    await waitFor(() => expect(checkedRows.value).toEqual(items))
+    await waitFor(() => expect(selectAllCheckbox).toHaveAttribute('checked'))
+
+    await wrapper.events.click(wrapper.getByLabelText('Deselect all entries'))
+
+    await waitFor(() => expect(rowCheckboxes[0]).not.toHaveAttribute('checked'))
+    expect(rowCheckboxes[1]).not.toHaveAttribute('checked')
+
+    await wrapper.events.click(rowCheckboxes[1])
+
+    expect(
+      await wrapper.findByLabelText('Deselect this entry'),
+    ).toBeInTheDocument()
+  })
+
+  it('supports disabling checkbox item for specific rows', async () => {
+    const checkedRows = ref([])
+
+    const items = [
+      {
+        id: 1,
+        checked: false,
+        disabled: true,
+        label: 'selection data 1',
+      },
+      {
+        id: 1,
+        checked: true,
+        disabled: true,
+        label: 'selection data 1',
+      },
+    ]
+
+    const wrapper = renderTable(
+      {
+        headers: [
+          {
+            key: 'urlTest',
+            label: 'Link Row',
+          },
+        ],
+        items,
+        hasCheckboxColumn: true,
+      },
+      { form: true, vModel: { checkedRows } },
+    )
+
+    const checkboxes = wrapper.getAllByRole('checkbox')
+    expect(checkboxes).toHaveLength(3)
+
+    expect(checkboxes[1]).toBeDisabled()
+    expect(checkboxes[1]).not.toBeChecked()
+    expect(checkboxes[2]).toHaveAttribute('value', 'true')
+
+    await wrapper.events.click(checkboxes[1])
+
+    expect(checkedRows.value).toEqual([])
+
+    await wrapper.events.click(checkboxes[0])
+
+    expect(checkedRows.value).toEqual([])
+  })
 })

+ 1 - 0
app/frontend/apps/desktop/components/CommonSimpleTable/__tests__/CommonSimpleTable.spec.ts.snapshot.txt

@@ -43,6 +43,7 @@
   <tbody>
     
     <simple-table-row-stub
+      hascheckbox="false"
       isrowselected="false"
       item="[object Object]"
     />

+ 3 - 2
app/frontend/apps/desktop/components/CommonSimpleTable/types.ts

@@ -1,6 +1,7 @@
 // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+import type { Props as CommonLinkProps } from '#shared/components/CommonLink/CommonLink.vue'
 
-type TableColumnType = 'timestamp' | 'timestamp_absolute'
+type TableColumnType = 'timestamp' | 'timestamp_absolute' | 'link'
 
 export interface TableHeader<K = string> {
   key: K
@@ -15,6 +16,6 @@ export interface TableHeader<K = string> {
   [key: string]: unknown
 }
 export interface TableItem {
-  [key: string]: unknown
+  [key: string]: unknown | Partial<CommonLinkProps>
   id: string | number
 }

+ 53 - 0
app/frontend/apps/desktop/components/CommonSimpleTable/useTableCheckboxes.ts

@@ -0,0 +1,53 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import { computed, type ModelRef, type Ref } from 'vue'
+
+import type { TableItem } from '#desktop/components/CommonSimpleTable/types.ts'
+
+export const useTableCheckboxes = (
+  checkedRowItems: ModelRef<TableItem[]>,
+  items: Ref<TableItem[]>,
+) => {
+  const allCheckboxRowsSelected = computed(
+    () => checkedRowItems.value.length >= items.value.length,
+  )
+
+  const selectAllRowCheckboxes = (value?: boolean) => {
+    if (allCheckboxRowsSelected.value === value) return
+
+    if (value) {
+      checkedRowItems.value = items.value
+    } else {
+      checkedRowItems.value = items.value.filter(
+        (item) => item.disabled && item.checked,
+      )
+    }
+  }
+
+  const handleCheckboxUpdate = (item: TableItem) => {
+    const isChecked = checkedRowItems.value.some(
+      (selectedItem) => selectedItem.id === item.id,
+    )
+
+    if (!isChecked) {
+      // Overwrite entire array to trigger reactivity since defineModel default value is not reactive.
+      checkedRowItems.value = [...checkedRowItems.value, item]
+    } else {
+      checkedRowItems.value = checkedRowItems.value.filter(
+        (selectedItem) => item.id.toString() !== selectedItem.id.toString(),
+      )
+    }
+  }
+
+  const hasCheckboxId = (itemId: string | number) =>
+    checkedRowItems.value.some(
+      (selectedItem) => selectedItem.id.toString() === itemId.toString(),
+    )
+
+  return {
+    allCheckboxRowsSelected,
+    selectAllRowCheckboxes,
+    handleCheckboxUpdate,
+    hasCheckboxId,
+  }
+}

+ 5 - 0
app/frontend/apps/desktop/initializer/assets/i-doit-logo-dark.svg

@@ -0,0 +1,5 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="12" cy="12" r="12" fill="#353535"/>
+  <path d="M12.0121 4.07993C10.8435 4.07809 10.0806 4.85696 10.08 5.87878C10.08 6.87922 10.8207 7.67967 11.9654 7.67993L11.9875 7.67967C13.1773 7.67993 13.9195 6.8802 13.92 5.88056C13.8974 4.85828 13.1794 4.07993 12.0121 4.07993" fill="#E42D2C"/>
+  <path d="M10.32 19.9196L13.6769 19.92L13.68 9.57055L10.3238 9.57004L10.32 19.9196Z" fill="white"/>
+</svg>

+ 5 - 0
app/frontend/apps/desktop/initializer/assets/i-doit-logo-light.svg

@@ -0,0 +1,5 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="12" cy="12" r="12" fill="white"/>
+<path d="M12.0121 4.07993C10.8435 4.07809 10.0805 4.85696 10.08 5.87878C10.08 6.87922 10.8207 7.67967 11.9654 7.67993L11.9874 7.67967C13.1773 7.67993 13.9195 6.8802 13.92 5.88056C13.8974 4.85828 13.1794 4.07993 12.0121 4.07993" fill="#E42D2C"/>
+<path d="M10.32 19.9196L13.6769 19.92L13.68 9.57055L10.3238 9.57004L10.32 19.9196Z" fill="#171E24"/>
+</svg>

+ 154 - 0
app/frontend/apps/desktop/pages/ticket/__tests__/ticket-create-idoit.spec.ts

@@ -0,0 +1,154 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import { waitFor, within } from '@testing-library/vue'
+import { expect } from 'vitest'
+
+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 { waitForTicketCreateMutationCalls } from '#shared/entities/ticket/graphql/mutations/create.mocks.ts'
+import getUuid from '#shared/utils/getUuid.ts'
+
+import {
+  handleCustomerMock,
+  handleMockFormUpdaterQuery,
+  handleMockUserQuery,
+} from '#desktop/pages/ticket/__tests__/support/ticket-create-helpers.ts'
+import { mockTicketExternalReferencesIdoitObjectListQuery } from '#desktop/pages/ticket/graphql/queries/ticketExternalReferencesIdoitObjectList.mocks.ts'
+import { mockTicketExternalReferencesIdoitObjectSearchQuery } from '#desktop/pages/ticket/graphql/queries/ticketExternalReferencesIdoitObjectSearch.mocks.ts'
+
+describe('Ticket create i-doit links', () => {
+  describe('ticket creation', () => {
+    it('submits a new ticket with i-doit objects', async () => {
+      await mockApplicationConfig({
+        idoit_integration: true,
+        ui_task_mananger_max_task_count: 30,
+        ui_ticket_create_available_types: [
+          'phone-in',
+          'phone-out',
+          'email-out',
+        ],
+      })
+      mockPermissions(['ticket.agent'])
+      handleMockFormUpdaterQuery({
+        pending_time: {
+          show: false,
+        },
+      })
+
+      mockTicketExternalReferencesIdoitObjectListQuery({
+        ticketExternalReferencesIdoitObjectList: [],
+      })
+      const uid = getUuid()
+
+      const view = await visitView(`/ticket/create/${uid}`)
+
+      await view.events.click(view.getByRole('button', { name: 'i-doit' }))
+
+      await waitFor(() =>
+        expect(
+          view.getByRole('heading', { level: 1, name: 'New Ticket' }),
+        ).toBeInTheDocument(),
+      )
+
+      await view.events.type(view.getByLabelText('Title'), 'Test Ticket')
+
+      await handleCustomerMock(view)
+
+      handleMockUserQuery()
+
+      await view.events.click(
+        view.getByRole('option', {
+          name: 'Avatar (Nicole Braun) Nicole Braun – Zammad Foundation',
+        }),
+      )
+
+      await view.events.click(view.getByLabelText('Text'))
+      await view.events.type(view.getByLabelText('Text'), 'Test ticket text')
+
+      await view.events.click(view.getByLabelText('Group'))
+      await view.events.click(view.getByRole('option', { name: 'Users' }))
+
+      await view.events.click(view.getByLabelText('Priority'))
+      await view.events.click(view.getByRole('option', { name: '2 normal' }))
+
+      await view.events.click(view.getByLabelText('State'))
+      await view.events.click(view.getByRole('option', { name: 'open' }))
+
+      const sidebar = view.getByLabelText('Content sidebar')
+
+      mockTicketExternalReferencesIdoitObjectSearchQuery({
+        ticketExternalReferencesIdoitObjectSearch: [
+          {
+            idoitObjectId: 26,
+            link: 'http://localhost:9001/?objID=26',
+            title: 'Test',
+            type: 'Building',
+            status: 'in operation',
+          },
+        ],
+      })
+
+      await view.events.click(
+        await within(sidebar).findByRole('button', {
+          name: 'Link Objects',
+        }),
+      )
+
+      const flyout = await view.findByRole('complementary', {
+        name: 'i-doit: Link objects',
+      })
+
+      await view.events.click(await within(flyout).findByText('26'))
+
+      await view.events.click(
+        within(flyout).getByRole('button', { name: 'Link Objects' }),
+      )
+
+      await view.events.click(view.getByRole('button', { name: 'Create' }))
+
+      const calls = await waitForTicketCreateMutationCalls()
+      expect(calls.at(-1)?.variables.input).toEqual(
+        expect.objectContaining({
+          externalReferences: {
+            idoit: expect.anything(), // :TODO automock does not returns the right [26] id instead a random generated object ids
+          },
+        }),
+      )
+    })
+  })
+
+  it('displays i-doit integration', async () => {
+    mockApplicationConfig({
+      idoit_integration: true,
+    })
+    mockPermissions(['ticket.agent'])
+
+    const uid = getUuid()
+    const view = await visitView(`/ticket/create/${uid}`)
+
+    const sidebar = view.getByLabelText('Content sidebar')
+
+    expect(
+      within(sidebar).getByRole('button', { name: 'i-doit' }),
+    ).toBeInTheDocument()
+  })
+
+  it('hides i-doit integration when not available', async () => {
+    mockPermissions(['ticket.agent'])
+
+    mockApplicationConfig({
+      idoit_integration: false,
+    })
+
+    const uid = getUuid()
+    const view = await visitView(`/ticket/create/${uid}`)
+
+    const sidebar = view.getByLabelText('Content sidebar')
+
+    expect(
+      within(sidebar).queryByRole('button', { name: 'i-doit' }),
+    ).not.toBeInTheDocument()
+  })
+})

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