| <template> |
| <div class="d-flex align-center justify-space-between"> |
| <div> |
| <span v-if="!modelValue || Object.keys(modelValue).length === 0" style="color: rgb(var(--v-theme-primaryText));"> |
| {{ t('core.common.objectEditor.noItems') }} |
| </span> |
| <div v-else class="d-flex flex-wrap ga-2"> |
| <v-chip v-for="key in displayKeys" :key="key" size="x-small" label color="primary"> |
| {{ key.length > 20 ? key.slice(0, 20) + '...' : key }} |
| </v-chip> |
| <v-chip v-if="Object.keys(modelValue).length > maxDisplayItems" size="x-small" label color="grey-lighten-1"> |
| +{{ Object.keys(modelValue).length - maxDisplayItems }} |
| </v-chip> |
| </div> |
| </div> |
| <v-btn size="small" color="primary" variant="tonal" @click="openDialog"> |
| {{ resolveButtonText }} |
| </v-btn> |
| </div> |
| |
| |
| <v-dialog v-model="dialog" max-width="600px"> |
| <v-card> |
| <v-card-title class="text-h3 py-4" style="font-weight: normal;"> |
| {{ resolveDialogTitle }} |
| </v-card-title> |
| |
| <v-card-text class="pa-4" style="max-height: 400px; overflow-y: auto;"> |
| |
| <div v-if="nonTemplatePairs.length > 0"> |
| <div v-for="(pair, index) in nonTemplatePairs" :key="index" class="key-value-pair"> |
| <v-row no-gutters align="center" class="mb-2"> |
| <v-col cols="4"> |
| <v-text-field |
| v-model="pair.key" |
| density="compact" |
| variant="outlined" |
| hide-details |
| :placeholder="t('core.common.objectEditor.placeholders.keyName')" |
| @blur="updateKey(index, pair.key)" |
| ></v-text-field> |
| </v-col> |
| <v-col cols="7" class="pl-2 d-flex align-center justify-end"> |
| <v-text-field |
| v-if="pair.type === 'string'" |
| v-model="pair.value" |
| density="compact" |
| variant="outlined" |
| hide-details |
| :placeholder="t('core.common.objectEditor.placeholders.stringValue')" |
| ></v-text-field> |
| <div v-else-if="pair.type === 'number' || pair.type === 'float' || pair.type === 'int'" class="d-flex align-center gap-2 flex-grow-1"> |
| <v-slider |
| v-if="pair.slider" |
| :model-value="Number(pair.value) || 0" |
| @update:model-value="pair.value = $event" |
| :min="pair.slider.min" |
| :max="pair.slider.max" |
| :step="pair.slider.step" |
| color="primary" |
| density="compact" |
| hide-details |
| class="flex-grow-1" |
| ></v-slider> |
| <v-text-field |
| v-model.number="pair.value" |
| type="number" |
| density="compact" |
| variant="outlined" |
| hide-details |
| :placeholder="t('core.common.objectEditor.placeholders.numberValue')" |
| :style="pair.slider ? 'max-width: 120px;' : ''" |
| ></v-text-field> |
| </div> |
| <v-switch |
| v-else-if="pair.type === 'boolean'" |
| v-model="pair.value" |
| density="compact" |
| hide-details |
| color="primary" |
| ></v-switch> |
| <v-text-field |
| v-if="pair.type === 'json'" |
| v-model="pair.value" |
| density="compact" |
| variant="outlined" |
| hide-details="auto" |
| :placeholder="t('core.common.objectEditor.placeholders.jsonValue')" |
| @blur="updateJSON(index, pair.value)" |
| :error-messages="pair.jsonError" |
| ></v-text-field> |
| </v-col> |
| <v-col cols="1" class="pl-2"> |
| <v-btn |
| icon |
| variant="text" |
| size="small" |
| color="error" |
| @click="removeKeyValuePairByKey(pair.key)" |
| > |
| <v-icon>mdi-delete</v-icon> |
| </v-btn> |
| </v-col> |
| </v-row> |
| </div> |
| </div> |
| |
| |
| <div v-if="hasTemplateSchema" class="mt-4"> |
| <v-divider class="mb-3"></v-divider> |
| <div class="text-caption text-grey mb-2">{{ t('core.common.objectEditor.presets') }}</div> |
| <div v-for="(template, templateKey) in templateSchema" :key="templateKey" class="template-field" :class="{ 'template-field-inactive': !isTemplateKeyAdded(templateKey) }"> |
| <v-row no-gutters align="center" class="mb-2"> |
| <v-col cols="4"> |
| <div class="d-flex flex-column"> |
| <span class="text-caption font-weight-medium">{{ getTemplateTitle(template, templateKey) }}</span> |
| <span v-if="template.hint" class="text-caption text-grey" style="font-size: 0.7rem;">{{ translateIfKey(template.hint) }}</span> |
| </div> |
| </v-col> |
| <v-col cols="7" class="pl-2 d-flex align-center justify-end"> |
| <v-text-field |
| v-if="template.type === 'string'" |
| :model-value="getTemplateValue(templateKey)" |
| @update:model-value="updateTemplateValue(templateKey, $event)" |
| density="compact" |
| variant="outlined" |
| hide-details |
| :placeholder="t('core.common.objectEditor.placeholders.stringValue')" |
| ></v-text-field> |
| <div v-else-if="template.type === 'number' || template.type === 'float' || template.type === 'int'" class="d-flex align-center ga-4 flex-grow-1"> |
| <v-slider |
| v-if="template.slider" |
| :model-value="Number(getTemplateValue(templateKey)) || 0" |
| @update:model-value="updateTemplateValue(templateKey, $event)" |
| :min="template.slider.min" |
| :max="template.slider.max" |
| :step="template.slider.step" |
| color="primary" |
| density="compact" |
| hide-details |
| class="flex-grow-1" |
| ></v-slider> |
| <v-text-field |
| :model-value="getTemplateValue(templateKey)" |
| @update:model-value="updateTemplateValue(templateKey, $event)" |
| type="number" |
| density="compact" |
| variant="outlined" |
| hide-details |
| :placeholder="t('core.common.objectEditor.placeholders.numberValue')" |
| :style="template.slider ? 'max-width: 120px;' : ''" |
| ></v-text-field> |
| </div> |
| <v-switch |
| v-else-if="template.type === 'boolean' || template.type === 'bool'" |
| :model-value="getTemplateValue(templateKey)" |
| @update:model-value="updateTemplateValue(templateKey, $event)" |
| density="compact" |
| hide-details |
| color="primary" |
| ></v-switch> |
| </v-col> |
| <v-col cols="1" class="pl-2"> |
| <v-btn |
| v-if="isTemplateKeyAdded(templateKey)" |
| icon |
| variant="text" |
| size="small" |
| color="error" |
| @click="removeTemplateKey(templateKey)" |
| > |
| <v-icon>mdi-close</v-icon> |
| </v-btn> |
| </v-col> |
| </v-row> |
| </div> |
| </div> |
| |
| <div v-if="localKeyValuePairs.length === 0 && !hasTemplateSchema" class="text-center py-8"> |
| <v-icon size="64" color="grey-lighten-1">mdi-code-json</v-icon> |
| <p class="text-grey mt-4">{{ t('core.common.objectEditor.noParams') }}</p> |
| </div> |
| </v-card-text> |
| |
| |
| <v-card-text class="pa-4"> |
| <div class="d-flex align-center ga-2"> |
| <v-text-field |
| v-model="newKey" |
| :label="t('core.common.objectEditor.newKeyLabel')" |
| density="compact" |
| variant="outlined" |
| hide-details |
| class="flex-grow-1" |
| ></v-text-field> |
| <v-select |
| v-model="newValueType" |
| :items="['string', 'number', 'boolean', 'json']" |
| :label="t('core.common.objectEditor.valueTypeLabel')" |
| density="compact" |
| variant="outlined" |
| hide-details |
| style="max-width: 120px;" |
| ></v-select> |
| <v-btn @click="addKeyValuePair" variant="tonal" color="primary"> |
| <v-icon>mdi-plus</v-icon> |
| {{ t('core.common.add') }} |
| </v-btn> |
| </div> |
| </v-card-text> |
| |
| <v-card-actions class="pa-4"> |
| <v-spacer></v-spacer> |
| <v-btn variant="text" @click="cancelDialog">{{ t('core.common.cancel') }}</v-btn> |
| <v-btn color="primary" @click="confirmDialog">{{ t('core.common.confirm') }}</v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| </template> |
| |
| <script setup> |
| import { ref, computed, watch } from 'vue' |
| import { useI18n, useModuleI18n } from '@/i18n/composables' |
| |
| const { t } = useI18n() |
| const { tm, getRaw } = useModuleI18n('features/config-metadata') |
| |
| const props = defineProps({ |
| modelValue: { |
| type: Object, |
| required: true |
| }, |
| itemMeta: { |
| type: Object, |
| default: null |
| }, |
| buttonText: { |
| type: String, |
| default: '' |
| }, |
| dialogTitle: { |
| type: String, |
| default: '' |
| }, |
| maxDisplayItems: { |
| type: Number, |
| default: 1 |
| } |
| }) |
| |
| const emit = defineEmits(['update:modelValue']) |
| |
| const resolveButtonText = computed(() => props.buttonText || t('core.common.list.modifyButton')) |
| const resolveDialogTitle = computed(() => props.dialogTitle || t('core.common.objectEditor.dialogTitle')) |
| |
| const dialog = ref(false) |
| const localKeyValuePairs = ref([]) |
| const originalKeyValuePairs = ref([]) |
| const newKey = ref('') |
| const newValueType = ref('string') |
| |
| |
| const templateSchema = computed(() => { |
| return props.itemMeta?.template_schema || {} |
| }) |
| |
| const hasTemplateSchema = computed(() => { |
| return Object.keys(templateSchema.value).length > 0 |
| }) |
| |
| |
| const displayKeys = computed(() => { |
| return Object.keys(props.modelValue).slice(0, props.maxDisplayItems) |
| }) |
| |
| |
| const nonTemplatePairs = computed(() => { |
| return localKeyValuePairs.value.filter(pair => !templateSchema.value[pair.key]) |
| }) |
| |
| |
| watch(() => props.modelValue, (newValue) => { |
| |
| |
| }, { immediate: true }) |
| |
| function initializeLocalKeyValuePairs() { |
| localKeyValuePairs.value = [] |
| for (const [key, value] of Object.entries(props.modelValue)) { |
| let _type = (typeof value) === 'object' ? 'json':(typeof value) |
| let _value = _type === 'json'?JSON.stringify(value):value |
| |
| |
| const template = templateSchema.value[key] |
| if (template) { |
| |
| _type = template.type || _type |
| |
| if (_value === undefined || _value === null) { |
| _value = template.default !== undefined ? template.default : _value |
| } |
| } |
| |
| localKeyValuePairs.value.push({ |
| key: key, |
| value: _value, |
| type: _type, |
| slider: template?.slider, |
| template: template |
| }) |
| } |
| } |
| |
| function openDialog() { |
| initializeLocalKeyValuePairs() |
| originalKeyValuePairs.value = JSON.parse(JSON.stringify(localKeyValuePairs.value)) |
| newKey.value = '' |
| newValueType.value = 'string' |
| dialog.value = true |
| } |
| |
| function addKeyValuePair() { |
| const key = newKey.value.trim() |
| if (key !== '') { |
| const isKeyExists = localKeyValuePairs.value.some(pair => pair.key === key) |
| if (isKeyExists) { |
| alert(t('core.common.objectEditor.keyExists')) |
| return |
| } |
| |
| let defaultValue |
| switch (newValueType.value) { |
| case 'number': |
| defaultValue = 0 |
| break |
| case 'boolean': |
| defaultValue = false |
| break |
| case 'json': |
| defaultValue = "{}" |
| break |
| default: |
| defaultValue = "" |
| break |
| } |
| |
| localKeyValuePairs.value.push({ |
| key: key, |
| value: defaultValue, |
| type: newValueType.value |
| }) |
| newKey.value = '' |
| } |
| } |
| |
| function updateJSON(index, newValue) { |
| try { |
| JSON.parse(newValue) |
| localKeyValuePairs.value[index].jsonError = '' |
| } catch (e) { |
| localKeyValuePairs.value[index].jsonError = t('core.common.objectEditor.invalidJson') |
| } |
| } |
| |
| function removeKeyValuePairByKey(key) { |
| const index = localKeyValuePairs.value.findIndex(pair => pair.key === key) |
| if (index >= 0) { |
| localKeyValuePairs.value.splice(index, 1) |
| } |
| } |
| |
| function updateKey(index, newKey) { |
| const originalKey = localKeyValuePairs.value[index].key |
| |
| if (originalKey === newKey) return |
| |
| |
| const isKeyExists = localKeyValuePairs.value.some((pair, i) => i !== index && pair.key === newKey) |
| if (isKeyExists) { |
| |
| alert(t('core.common.objectEditor.keyExists')) |
| |
| localKeyValuePairs.value[index].key = originalKey |
| return |
| } |
| |
| |
| const template = templateSchema.value[newKey] |
| if (template) { |
| |
| localKeyValuePairs.value[index].type = template.type || localKeyValuePairs.value[index].type |
| if (localKeyValuePairs.value[index].value === undefined || localKeyValuePairs.value[index].value === null || localKeyValuePairs.value[index].value === '') { |
| localKeyValuePairs.value[index].value = template.default !== undefined ? template.default : localKeyValuePairs.value[index].value |
| } |
| localKeyValuePairs.value[index].slider = template.slider |
| localKeyValuePairs.value[index].template = template |
| } else { |
| |
| localKeyValuePairs.value[index].slider = undefined |
| localKeyValuePairs.value[index].template = undefined |
| } |
| |
| |
| localKeyValuePairs.value[index].key = newKey |
| } |
| |
| function isTemplateKeyAdded(templateKey) { |
| return localKeyValuePairs.value.some(pair => pair.key === templateKey) |
| } |
| |
| function getTemplateValue(templateKey) { |
| const pair = localKeyValuePairs.value.find(pair => pair.key === templateKey) |
| if (pair) { |
| return pair.value |
| } |
| const template = templateSchema.value[templateKey] |
| return template?.default !== undefined ? template.default : getDefaultValueForType(template?.type || 'string') |
| } |
| |
| function updateTemplateValue(templateKey, newValue) { |
| const existingIndex = localKeyValuePairs.value.findIndex(pair => pair.key === templateKey) |
| const template = templateSchema.value[templateKey] |
| |
| if (existingIndex >= 0) { |
| |
| localKeyValuePairs.value[existingIndex].value = newValue |
| } else { |
| |
| let valueType = template?.type || 'string' |
| localKeyValuePairs.value.push({ |
| key: templateKey, |
| value: newValue, |
| type: valueType, |
| slider: template?.slider, |
| template: template |
| }) |
| } |
| } |
| |
| function removeTemplateKey(templateKey) { |
| const index = localKeyValuePairs.value.findIndex(pair => pair.key === templateKey) |
| if (index >= 0) { |
| localKeyValuePairs.value.splice(index, 1) |
| } |
| } |
| |
| function getDefaultValueForType(type) { |
| switch (type) { |
| case 'int': |
| case 'float': |
| case 'number': |
| return 0 |
| case 'bool': |
| case 'boolean': |
| return false |
| case 'json': |
| return "{}" |
| case 'string': |
| default: |
| return "" |
| } |
| } |
| |
| function confirmDialog() { |
| const updatedValue = {} |
| for (const pair of localKeyValuePairs.value) { |
| if (pair.type === 'json' && pair.jsonError) return |
| let convertedValue = pair.value |
| |
| switch (pair.type) { |
| case 'int': |
| convertedValue = parseInt(pair.value) || 0 |
| break |
| case 'float': |
| case 'number': |
| |
| convertedValue = Number(pair.value) |
| |
| |
| break |
| case 'bool': |
| case 'boolean': |
| |
| |
| |
| |
| break |
| case 'json': |
| convertedValue = JSON.parse(pair.value) |
| break |
| case 'string': |
| default: |
| |
| convertedValue = String(pair.value) |
| break |
| } |
| updatedValue[pair.key] = convertedValue |
| } |
| emit('update:modelValue', updatedValue) |
| dialog.value = false |
| } |
| |
| function cancelDialog() { |
| |
| localKeyValuePairs.value = JSON.parse(JSON.stringify(originalKeyValuePairs.value)) |
| dialog.value = false |
| } |
| |
| function translateIfKey(value) { |
| if (!value || typeof value !== 'string') return value |
| return getRaw(value) ? tm(value) : value |
| } |
| |
| function getTemplateTitle(template, templateKey) { |
| return translateIfKey(template?.name || template?.description || templateKey) |
| } |
| </script> |
| |
| <style scoped> |
| .key-value-pair { |
| width: 100%; |
| } |
| |
| .template-field { |
| transition: opacity 0.2s; |
| } |
| |
| .template-field-inactive { |
| opacity: 0.8; |
| } |
| </style> |
| |