308 lines
8.2 KiB
Vue
308 lines
8.2 KiB
Vue
<template>
|
|
<div class="property-widget">
|
|
<!-- Boolean type - checkbox -->
|
|
<v-checkbox
|
|
v-if="descriptor?.propertyType === 'Boolean'"
|
|
:model-value="modelValue"
|
|
:disabled="disabled"
|
|
hide-details
|
|
density="compact"
|
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
/>
|
|
|
|
<!-- Array type - multiple widgets -->
|
|
<div v-else-if="descriptor?.array">
|
|
<div
|
|
v-for="(item, index) in modelValue || []"
|
|
:key="String(index)"
|
|
class="d-flex align-center mb-2"
|
|
>
|
|
<PropertyWidget
|
|
:model-value="item"
|
|
:descriptor="{ ...descriptor, array: false }"
|
|
:disabled="disabled"
|
|
class="flex-grow-1"
|
|
@update:model-value="updateArrayItem(Number(index), $event)"
|
|
/>
|
|
|
|
<div class="d-flex flex-column ml-2">
|
|
<v-btn
|
|
icon="mdi-chevron-up"
|
|
size="x-small"
|
|
variant="text"
|
|
:disabled="disabled || index === 0"
|
|
@click="moveUp(Number(index))"
|
|
/>
|
|
<v-btn
|
|
icon="mdi-chevron-down"
|
|
size="x-small"
|
|
variant="text"
|
|
:disabled="disabled || index === (modelValue?.length || 0) - 1"
|
|
@click="moveDown(Number(index))"
|
|
/>
|
|
</div>
|
|
|
|
<v-btn
|
|
icon="mdi-close"
|
|
size="x-small"
|
|
variant="text"
|
|
color="error"
|
|
:disabled="disabled"
|
|
class="ml-1"
|
|
@click="removeItem(Number(index))"
|
|
/>
|
|
</div>
|
|
|
|
<v-btn
|
|
prepend-icon="mdi-plus"
|
|
size="small"
|
|
variant="outlined"
|
|
:disabled="disabled"
|
|
@click="addItem"
|
|
>
|
|
Добавить
|
|
</v-btn>
|
|
</div>
|
|
|
|
<!-- Text type -->
|
|
<v-textarea
|
|
v-if="descriptor?.propertyType === 'Text' && stringConstraint?.multiline"
|
|
:model-value="modelValue"
|
|
:disabled="disabled"
|
|
:rules="textValidationRules"
|
|
density="compact"
|
|
variant="outlined"
|
|
:maxlength="stringConstraint?.maxLength"
|
|
rows="3"
|
|
auto-grow
|
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
/>
|
|
|
|
<v-text-field
|
|
v-else-if="descriptor?.propertyType === 'Text'"
|
|
:model-value="modelValue"
|
|
:disabled="disabled"
|
|
:rules="textValidationRules"
|
|
density="compact"
|
|
variant="outlined"
|
|
:maxlength="stringConstraint?.maxLength"
|
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
/>
|
|
|
|
<!-- Number type -->
|
|
<div v-else-if="descriptor?.propertyType === 'Number'">
|
|
<!-- Range slider for numbers with step > 0 -->
|
|
<v-slider
|
|
v-if="useSlider"
|
|
:model-value="modelValue"
|
|
:disabled="disabled"
|
|
hide-details
|
|
density="compact"
|
|
:min="numberConstraint?.min"
|
|
:max="numberConstraint?.max"
|
|
:step="numberConstraint?.step"
|
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
>
|
|
<template #append>
|
|
<v-text-field
|
|
:model-value="modelValue"
|
|
:disabled="disabled"
|
|
hide-details
|
|
density="compact"
|
|
variant="outlined"
|
|
style="width: 100px"
|
|
type="number"
|
|
:min="numberConstraint?.min"
|
|
:max="numberConstraint?.max"
|
|
:step="numberConstraint?.step"
|
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
/>
|
|
</template>
|
|
</v-slider>
|
|
|
|
<!-- Text field for other numbers -->
|
|
<v-text-field
|
|
v-else
|
|
:model-value="modelValue"
|
|
:disabled="disabled"
|
|
hide-details
|
|
density="compact"
|
|
variant="outlined"
|
|
type="number"
|
|
:min="numberConstraint?.min"
|
|
:max="numberConstraint?.max"
|
|
:step="numberConstraint?.step || (numberConstraint?.integer ? 1 : 0.1)"
|
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
/>
|
|
</div>
|
|
|
|
<!-- ValueList type - dropdown -->
|
|
<v-select
|
|
v-else-if="descriptor?.propertyType === 'ValueList'"
|
|
:model-value="modelValue"
|
|
:disabled="disabled"
|
|
hide-details
|
|
density="compact"
|
|
variant="outlined"
|
|
:items="valueListConstraint?.values || []"
|
|
item-title="label"
|
|
item-value="value"
|
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
/>
|
|
|
|
<!-- JSON type - textarea -->
|
|
<v-textarea
|
|
v-else-if="descriptor?.propertyType === 'JSON'"
|
|
:model-value="
|
|
typeof modelValue === 'object' ? JSON.stringify(modelValue, null, 2) : modelValue
|
|
"
|
|
:disabled="disabled"
|
|
hide-details
|
|
density="compact"
|
|
variant="outlined"
|
|
rows="5"
|
|
@update:model-value="updateJsonValue"
|
|
/>
|
|
|
|
<!-- Fallback for unknown types -->
|
|
<v-text-field
|
|
v-else
|
|
:model-value="modelValue"
|
|
:disabled="disabled"
|
|
hide-details
|
|
density="compact"
|
|
variant="outlined"
|
|
placeholder="Unknown type"
|
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import type {
|
|
PropertyDescriptor,
|
|
NumberConstraint,
|
|
ValueListConstraint,
|
|
StringConstraint
|
|
} from '@/generated/RpcClient'
|
|
|
|
interface Props {
|
|
modelValue: any
|
|
descriptor?: PropertyDescriptor
|
|
disabled?: boolean
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'update:modelValue', value: any): void
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const emit = defineEmits<Emits>()
|
|
|
|
const stringConstraint = computed((): StringConstraint | undefined => {
|
|
return props.descriptor?.constraints?.find(
|
|
(c) => c.type === 'StringConstraint'
|
|
) as StringConstraint
|
|
})
|
|
|
|
const numberConstraint = computed((): NumberConstraint | undefined => {
|
|
return props.descriptor?.constraints?.find(
|
|
(c) => c.type === 'NumberConstraint'
|
|
) as NumberConstraint
|
|
})
|
|
|
|
const valueListConstraint = computed((): ValueListConstraint | undefined => {
|
|
return props.descriptor?.constraints?.find(
|
|
(c) => c.type === 'ValueListConstraint'
|
|
) as ValueListConstraint
|
|
})
|
|
|
|
const useSlider = computed((): boolean => {
|
|
const constraint = numberConstraint.value
|
|
return constraint ? constraint.step > 0 : false
|
|
})
|
|
|
|
const textValidationRules = computed(() => {
|
|
const rules: any[] = []
|
|
|
|
if (stringConstraint.value?.regexp) {
|
|
const regex = new RegExp(stringConstraint.value.regexp)
|
|
rules.push((value: string) => {
|
|
if (!value) return true // Allow empty values
|
|
return regex.test(value) || 'Значение не соответствует требуемому формату'
|
|
})
|
|
}
|
|
|
|
return rules
|
|
})
|
|
|
|
const updateJsonValue = (newValue: string) => {
|
|
try {
|
|
if (!newValue.trim()) {
|
|
emit('update:modelValue', null)
|
|
return
|
|
}
|
|
const parsed = JSON.parse(newValue)
|
|
emit('update:modelValue', parsed)
|
|
} catch (e) {
|
|
// Keep invalid JSON as string for now
|
|
emit('update:modelValue', newValue)
|
|
}
|
|
}
|
|
|
|
const updateArrayItem = (index: number, newValue: any) => {
|
|
const newArray = [...(props.modelValue || [])]
|
|
newArray[index] = newValue
|
|
emit('update:modelValue', newArray)
|
|
}
|
|
|
|
const addItem = () => {
|
|
const newArray = [...(props.modelValue || [])]
|
|
|
|
// Determine default value based on property type
|
|
let defaultValue: any = ''
|
|
if (props.descriptor?.propertyType === 'Boolean') {
|
|
defaultValue = false
|
|
} else if (props.descriptor?.propertyType === 'Number') {
|
|
defaultValue = 0
|
|
} else if (props.descriptor?.propertyType === 'JSON') {
|
|
defaultValue = {}
|
|
}
|
|
|
|
newArray.push(defaultValue)
|
|
emit('update:modelValue', newArray)
|
|
}
|
|
|
|
const removeItem = (index: number) => {
|
|
const newArray = [...(props.modelValue || [])]
|
|
newArray.splice(index, 1)
|
|
emit('update:modelValue', newArray)
|
|
}
|
|
|
|
const moveUp = (index: number) => {
|
|
if (index === 0) return
|
|
const newArray = [...(props.modelValue || [])]
|
|
const temp = newArray[index]
|
|
newArray[index] = newArray[index - 1]
|
|
newArray[index - 1] = temp
|
|
emit('update:modelValue', newArray)
|
|
}
|
|
|
|
const moveDown = (index: number) => {
|
|
const array = props.modelValue || []
|
|
if (index === array.length - 1) return
|
|
const newArray = [...array]
|
|
const temp = newArray[index]
|
|
newArray[index] = newArray[index + 1]
|
|
newArray[index + 1] = temp
|
|
emit('update:modelValue', newArray)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.property-widget {
|
|
width: 100%;
|
|
}
|
|
</style>
|