All files / src/composables useEntityItemReference.ts

88.46% Statements 46/52
69.04% Branches 29/42
80% Functions 12/15
95.23% Lines 40/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110                                              2x 2x 2x 2x 2x 1x         1x     1x       1x     6x 6x   6x 6x   6x 2x   2x   2x     6x 5x           6x     9x 8x 1x 1x   7x 7x 7x     6x 9x 9x   1x   7x       6x 5x 5x     5x 6x   1x 1x           6x              
import { computed, onMounted, ref, watch } from 'vue'
import type { Ref } from 'vue'
import { getActivePinia } from 'pinia'
import { useEntitiesStore } from '@/stores/entities'
import api from '@/services/api'
import type { ReferenceOption } from './useFlowMemberReference'
 
interface EntityItem {
  id: string
  [key: string]: any
}
 
/**
 * Derive a human-readable label for an item.
 *
 * The label is built from the first text/email/number/select field found in the
 * entity's field definitions. Falls back to the item ID when no suitable field
 * is found or when the value is empty.
 */
function buildItemLabel(
  item: EntityItem,
  fieldDefs: { fieldId?: string; type?: string }[]
): string {
  const labelFieldTypes = ['text', 'email', 'number', 'select', 'textarea']
  for (const field of fieldDefs) {
    Iif (!field.fieldId || !labelFieldTypes.includes(field.type ?? '')) continue
    const raw = item[field.fieldId]
    if (!raw && raw !== 0) continue
    Iif (typeof raw === 'object') {
      // Multilingual: pick any available language
      const first = Object.values(raw).find((v) => v)
      if (first) return String(first)
    } else {
      return String(raw)
    }
  }
  return item.id
}
 
/** Per-entity item cache — avoids refetching on every render. */
const itemCache = new Map<string, EntityItem[]>()
 
export function useEntityItemReference(entityId: Ref<string | undefined>) {
  const hasPinia = !!getActivePinia()
  const entitiesStore = hasPinia ? useEntitiesStore() : null
 
  const items = ref<EntityItem[]>([])
  const loading = ref(false)
 
  const fieldDefs = computed(() => {
    Iif (!entitiesStore || !entityId.value) return []
    const entity =
      entitiesStore.entities.find((e) => e.id === entityId.value) ??
      (entitiesStore.currentEntity?.id === entityId.value ? entitiesStore.currentEntity : null)
    return entity?.fields ?? []
  })
 
  const itemOptions = computed<ReferenceOption[]>(() =>
    items.value.map((item) => ({
      value: item.id,
      label: buildItemLabel(item, fieldDefs.value)
    }))
  )
 
  const itemLabelById = computed(() => new Map(itemOptions.value.map((o) => [o.value, o.label])))
 
  async function ensureItemsLoaded(force = false) {
    if (!entityId.value) return
    if (!force && itemCache.has(entityId.value)) {
      items.value = itemCache.get(entityId.value)!
      return
    }
    loading.value = true
    try {
      const response = await api.get(`/v1/entities/${entityId.value}/items`, {
        params: { limit: 200 }
      })
      const fetched: EntityItem[] = response.data.items ?? response.data ?? []
      itemCache.set(entityId.value, fetched)
      items.value = fetched
    } catch {
      items.value = []
    } finally {
      loading.value = false
    }
  }
 
  if (hasPinia) {
    onMounted(() => {
      void ensureItemsLoaded().catch(() => {})
    })
 
    watch(
      () => entityId.value,
      (newId, oldId) => {
        Eif (newId && newId !== oldId) {
          void ensureItemsLoaded().catch(() => {})
        }
      }
    )
  }
 
  return {
    itemOptions,
    itemLabelById,
    itemsLoading: loading,
    ensureItemsLoaded
  }
}