All files / src/components VariableInput.vue

0% Statements 0/88
100% Branches 1/1
100% Functions 1/1
0% Lines 0/88

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 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160                                                                                                                                                                                                                                                                                                                               
<script setup lang="ts">
/**
 * Renders the input for ONE variable VALUE. The caller decides whether
 * to render a single instance (``varScope = all``) or one instance per
 * scope slot (``varScope = team|user``) — this component knows nothing
 * about scopes. Its job is:
 *   * pick the right widget for the variable's HCL type
 *   * delegate OpenStack-typed variables to ``OpenStackResourcePicker``
 *   * bind to a single ``modelValue`` (v-model)
 *
 * File-typed variables are NOT handled here — the wizard renders the
 * ``FileDropZone`` block separately because file uploads travel through
 * a different draft channel (``fileUploads``) than scalar inputs.
 *
 * The visual styling (border / focus colors) is sourced from the
 * ``accent`` prop so both wizard sections (Packer = blue, Terraform =
 * purple) stay visually distinct without duplicating five separate
 * input variants per color.
 */
import OpenStackResourcePicker from '@/components/OpenStackResourcePicker.vue'
import { useI18n } from 'vue-i18n'
import type { AppVariable } from '@/types'
import type { OsResourceType } from '@/api/openstack-resources.api'
 
const { t } = useI18n()
 
const props = defineProps<{
  variable: AppVariable
  modelValue: any
  /** Which Network's id-mode value to consult when this is a subnet
   *  picker. Caller supplies it because cross-variable lookups live
   *  in the wizard's variables list, not in this component. */
  filterNetworkId?: string | null
  /** Accent color used for borders + focus ring; the wizard's two
   *  sections (Packer / Terraform) drive different palettes. */
  accent?: 'blue' | 'purple'
  /** DOM ``id`` to put on the underlying input. The single-input
   *  variant uses the variable name verbatim for ``<label>``
   *  click-targeting; scope-iterated variants pass a suffixed id so
   *  every slot has its own. */
  inputId?: string
  /** Disable the input — used when a previous wizard step needs to be
   *  completed first (e.g. no teams configured for a team-scoped var). */
  disabled?: boolean
}>()
 
const emit = defineEmits<{
  (e: 'update:modelValue', value: any): void
}>()
 
const isBool = (type: string) => ['bool', 'boolean'].includes(type.toLowerCase())
const isNumber = (type: string) => ['number', 'int', 'integer'].includes(type.toLowerCase())
const isList = (type: string) =>
  type.toLowerCase().startsWith('list') ||
  type.toLowerCase().startsWith('set') ||
  type.toLowerCase().startsWith('array')
 
// Picker hat Vorrang vor Type-basierten Eingaben — auch bei list(...)-
// Variablen, weil der Picker selbst Multi handhabt. File-Variablen
// werden vom Parent gerendert, daher hier explizit ausgenommen.
const hasOsPicker = (v: AppVariable): boolean =>
  Boolean(v.osType) && v.osType !== 'file'
 
// Narrow ``AppVariableOsType`` (which includes the pseudo-type ``file``)
// down to ``OsResourceType`` for the picker. ``hasOsPicker`` already
// guarantees ``file`` is filtered out at render time; this is the type
// system's view of the same check.
const pickerOsType = (v: AppVariable): OsResourceType => v.osType as OsResourceType
 
const update = (value: any) => emit('update:modelValue', value)
 
// Explicit class maps — Tailwind's JIT can't read class names assembled
// from template-literal segments. Listing both palettes here keeps
// every utility visible to the content scanner.
const borderClass = (() => {
  const a = props.accent || 'blue'
  return a === 'purple'
    ? 'border-purple-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-100'
    : 'border-blue-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100'
})()
const toggleOn = (() => {
  const a = props.accent || 'blue'
  return a === 'purple' ? 'bg-purple-500' : 'bg-blue-500'
})()
const toggleFocus = (() => {
  const a = props.accent || 'blue'
  return a === 'purple' ? 'focus:ring-2 focus:ring-purple-500' : 'focus:ring-2 focus:ring-blue-500'
})()
</script>
 
<template>
  <!-- OpenStack-Resource-Picker hat Vorrang über alle Type-basierten
       Renderings. Greift, sobald das Backend einen ``osType`` mitgegeben
       hat (außer ``file`` — das handled der Parent). -->
  <OpenStackResourcePicker
    v-if="hasOsPicker(variable)"
    :os-type="pickerOsType(variable)"
    :os-mode="variable.osMode || 'name'"
    :multi="variable.osMulti || false"
    :filter-network-id="variable.osType === 'subnet' ? (filterNetworkId ?? null) : null"
    :allow-free-text="true"
    :model-value="modelValue"
    @update:modelValue="update"
  />
 
  <div v-else-if="isBool(variable.type)" class="flex items-center gap-3">
    <button
      type="button"
      :disabled="disabled"
      @click="update(!modelValue)"
      class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
      :class="[modelValue ? toggleOn : 'bg-gray-300', toggleFocus]"
    >
      <span
        class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform shadow-sm"
        :class="modelValue ? 'translate-x-6' : 'translate-x-1'"
      />
    </button>
    <span class="text-sm font-medium text-gray-700">
      {{ modelValue ? t('variableInput.on') : t('variableInput.off') }}
    </span>
  </div>
 
  <input
    v-else-if="isNumber(variable.type)"
    :value="modelValue"
    @input="update(($event.target as HTMLInputElement).valueAsNumber)"
    type="number"
    :id="inputId || variable.name"
    :disabled="disabled"
    class="w-full px-3 py-2 rounded-lg border-2 outline-none transition-all font-medium text-gray-800 disabled:bg-gray-50 disabled:text-gray-500"
    :class="borderClass"
    placeholder="0"
  />
 
  <textarea
    v-else-if="isList(variable.type)"
    :value="modelValue"
    @input="update(($event.target as HTMLTextAreaElement).value)"
    :id="inputId || variable.name"
    :disabled="disabled"
    rows="3"
    class="w-full px-3 py-2 rounded-lg border-2 outline-none transition-all font-mono text-sm text-gray-800 disabled:bg-gray-50 disabled:text-gray-500"
    :class="borderClass"
    :placeholder="t('variableInput.listPlaceholder')"
  />
 
  <input
    v-else
    :value="modelValue"
    @input="update(($event.target as HTMLInputElement).value)"
    type="text"
    :id="inputId || variable.name"
    :disabled="disabled"
    class="w-full px-3 py-2 rounded-lg border-2 outline-none transition-all font-medium text-gray-800 disabled:bg-gray-50 disabled:text-gray-500"
    :class="borderClass"
    :placeholder="variable.default ? t('variableInput.defaultPlaceholder', { value: variable.default }) : t('variableInput.enterValue')"
  />
</template>