Добавил редактор property. Дополнил генерацию spec.
This commit is contained in:
parent
3d6b3d8059
commit
db88b2cf28
|
|
@ -118,14 +118,13 @@ public class SpecGenerator {
|
|||
continue;
|
||||
}
|
||||
var classAnnotation = type.getAnnotation(GenerateApiSpec.class);
|
||||
if (classAnnotation == null || !type.isInterface()) {
|
||||
if (classAnnotation == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var aClass : classAnnotation.directInheritors()) {
|
||||
if (!types.contains(aClass)) {
|
||||
types.add(aClass);
|
||||
typesQueue.add(aClass);
|
||||
typesQueue.add(registerType(aClass));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,8 +134,8 @@ public class SpecGenerator {
|
|||
var parents = typeDescriptor.putArray("parents");
|
||||
for (var aClass : type.getInterfaces()) {
|
||||
if (!types.contains(aClass)) {
|
||||
types.add(aClass);
|
||||
typesQueue.add(aClass);
|
||||
|
||||
typesQueue.add(registerType(aClass));
|
||||
}
|
||||
if (!aClass.isAnnotationPresent(GenerateApiSpec.class)) {
|
||||
continue;
|
||||
|
|
@ -145,6 +144,29 @@ public class SpecGenerator {
|
|||
}
|
||||
|
||||
var fields = typeDescriptor.putArray("fields");
|
||||
if (!type.isInterface()) {
|
||||
for (var field : type.getDeclaredFields()) {
|
||||
var annotation = field.getAnnotation(GenerateApiSpec.class);
|
||||
if (annotation == null) {
|
||||
continue;
|
||||
}
|
||||
var returnType = annotation.type();
|
||||
if (returnType == void.class) {
|
||||
returnType = field.getType();
|
||||
}
|
||||
|
||||
if (!types.contains(returnType)) {
|
||||
typesQueue.put(registerType(returnType));
|
||||
}
|
||||
|
||||
var fieldDescriptor = fields.addObject();
|
||||
fieldDescriptor.put("name", annotation.alias().isEmpty() ? field.getName() : annotation.alias());
|
||||
fieldDescriptor.put("type", getTypeName(returnType));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (var method : type.getMethods()) {
|
||||
var methodAnnotation = method.getAnnotation(GenerateApiSpec.class);
|
||||
if (methodAnnotation == null) {
|
||||
|
|
@ -157,8 +179,7 @@ public class SpecGenerator {
|
|||
}
|
||||
|
||||
if (!types.contains(returnType)) {
|
||||
types.add(returnType);
|
||||
typesQueue.put(returnType);
|
||||
typesQueue.put(registerType(returnType));
|
||||
}
|
||||
|
||||
var methodName = method.getName();
|
||||
|
|
@ -195,18 +216,20 @@ public class SpecGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
private void registerType(Class<?> type) {
|
||||
private Class<?> registerType(Class<?> type) {
|
||||
if (type.isArray()) {
|
||||
types.add(type.getComponentType());
|
||||
return type.getComponentType();
|
||||
} else {
|
||||
types.add(type);
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
private String getTypeName(Class<?> type) {
|
||||
if (type == null) return "unknown";
|
||||
if (type == boolean.class) return "boolean";
|
||||
if (type == int.class || type == long.class) return "number";
|
||||
if (type == int.class || type == long.class || type == double.class || type == float.class) return "number";
|
||||
if (type == String.class) return "string";
|
||||
if (type == void.class || type == Void.class) return "void";
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
|
|||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
|
||||
public @interface GenerateApiSpec {
|
||||
String alias() default "";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ package ru.kirillius.XCP.Persistence.Entities;
|
|||
|
||||
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||
import ru.kirillius.XCP.Persistence.PersistenceEntity;
|
||||
import ru.kirillius.XCP.Properties.Constraint;
|
||||
import ru.kirillius.XCP.Properties.Constraints;
|
||||
import ru.kirillius.XCP.Properties.PropertyType;
|
||||
|
||||
@GenerateApiSpec
|
||||
public interface PropertyDescriptor extends PersistenceEntity {
|
||||
@GenerateApiSpec
|
||||
@GenerateApiSpec(type = Constraint[].class)
|
||||
Constraints getConstraints();
|
||||
|
||||
void setConstraints(Constraints constraints);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
|||
})
|
||||
public interface Constraint {
|
||||
@JsonProperty(value = "type")
|
||||
default String type() {
|
||||
@GenerateApiSpec
|
||||
default String getType() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,15 @@ import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
|||
@NoArgsConstructor
|
||||
public final class NumberConstraint implements Constraint {
|
||||
@JsonProperty
|
||||
@GenerateApiSpec
|
||||
private double min;
|
||||
@JsonProperty
|
||||
@GenerateApiSpec
|
||||
private double max;
|
||||
@JsonProperty
|
||||
@GenerateApiSpec
|
||||
private double step;
|
||||
@JsonProperty
|
||||
@GenerateApiSpec
|
||||
private boolean integer;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,5 +12,14 @@ import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
|||
@NoArgsConstructor
|
||||
public final class StringConstraint implements Constraint {
|
||||
@JsonProperty
|
||||
@GenerateApiSpec
|
||||
private int maxLength;
|
||||
|
||||
@JsonProperty
|
||||
@GenerateApiSpec
|
||||
private boolean multiline;
|
||||
|
||||
@JsonProperty
|
||||
@GenerateApiSpec
|
||||
private String regexp;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package ru.kirillius.XCP.Properties;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.*;
|
||||
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
|
|
@ -14,5 +15,6 @@ import java.util.Collection;
|
|||
@NoArgsConstructor
|
||||
public final class ValueListConstraint implements Constraint {
|
||||
@JsonProperty
|
||||
@GenerateApiSpec(type = ArrayNode.class)
|
||||
private Collection<String> values;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,5 +9,22 @@
|
|||
"uuid": "00000000-0000-0000-0000-000000000000",
|
||||
"passwordHash": "$argon2id$v=19$m=65536,t=3,p=1$SBqQtx5adxoG53V0TgqmDw$zIy0Wiq53m9r/SOldtCXWXLWbvZuS0F3HHILxpUsLhQ"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "PropertyDescriptor",
|
||||
"entity": {
|
||||
"constraints": [
|
||||
{
|
||||
"type": "StringConstraint",
|
||||
"maxLength": 15,
|
||||
"multiline": false,
|
||||
"regexp": "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
||||
}
|
||||
],
|
||||
"array": false,
|
||||
"propertyType": "Text",
|
||||
"name": "host.ip",
|
||||
"uuid": "00bd0f8a-e9d8-4789-83e5-d2f2eb94f876"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -63,7 +63,8 @@ public final class RepositoryServiceImpl implements RepositoryService {
|
|||
InputRepositoryImpl.class,
|
||||
OutputRepositoryImpl.class,
|
||||
TagRepositoryImpl.class,
|
||||
UserRepositoryImpl.class
|
||||
UserRepositoryImpl.class,
|
||||
PropertyDescriptorRepositoryImpl.class
|
||||
);
|
||||
configuration = new Configuration();
|
||||
configuration.configure();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import ru.kirillius.XCP.Commons.Context;
|
|||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcServlet;
|
||||
import ru.kirillius.XCP.RPC.Services.Auth;
|
||||
import ru.kirillius.XCP.RPC.Services.Profile;
|
||||
import ru.kirillius.XCP.RPC.Services.Properties;
|
||||
import ru.kirillius.XCP.RPC.Services.Users;
|
||||
import ru.kirillius.XCP.Services.ServiceLoadPriority;
|
||||
import ru.kirillius.XCP.Services.WebService;
|
||||
|
|
@ -30,7 +31,8 @@ public class WebServiceImpl implements WebService {
|
|||
jsonRpc.registerRpcService(
|
||||
Users.class,
|
||||
Auth.class,
|
||||
Profile.class
|
||||
Profile.class,
|
||||
Properties.class
|
||||
);
|
||||
var config = context.getConfig();
|
||||
server = new Server(new InetSocketAddress(config.getHost(), config.getHttpPort()));
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ interface ApiModule {
|
|||
interface ApiType {
|
||||
name: string
|
||||
type: 'class' | 'enum'
|
||||
parents?: string[]
|
||||
fields?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
|
|
@ -77,8 +78,12 @@ ${values.map((value) => ` ${value} = "${value}"`).join(',\n')}
|
|||
}`
|
||||
} else if (apiType.type === 'class') {
|
||||
const fields = apiType.fields || []
|
||||
const fieldDefinitions = fields.map((field) => ` ${field.name}: ${field.type};`).join('\n')
|
||||
return `export interface ${apiType.name} {
|
||||
const fieldDefinitions = fields
|
||||
.map((field) => ` ${field.name}: ${field.type === 'array' ? '[]' : field.type};`)
|
||||
.join('\n')
|
||||
const parents = apiType.parents || []
|
||||
const extendsClause = parents.length > 0 ? ` extends ${parents.join(', ')}` : ''
|
||||
return `export interface ${apiType.name}${extendsClause} {
|
||||
${fieldDefinitions}
|
||||
}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,369 @@
|
|||
<template>
|
||||
<v-card elevation="2" class="property-editor">
|
||||
<v-card-title class="text-h6"> Свойства </v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="propertyForm" v-model="isFormValid">
|
||||
<v-table density="compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 35%">Свойство</th>
|
||||
<th style="width: 55%">Значение</th>
|
||||
<th style="width: 10%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(value, key) in modelValue" :key="key">
|
||||
<td class="align-middle">
|
||||
<div class="font-weight-medium">{{ key }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<!-- Edit mode - show widgets -->
|
||||
<template v-if="!disabled">
|
||||
<PropertyWidget
|
||||
v-if="getDescriptor(key)"
|
||||
v-model="modelValue[key]"
|
||||
:descriptor="getDescriptor(key)"
|
||||
:disabled="disabled"
|
||||
@update:model-value="updateValue(key, $event)"
|
||||
/>
|
||||
<!-- Fallback for unknown properties -->
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="modelValue[key]"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
placeholder="Введите значение"
|
||||
@update:model-value="updateValue(key, $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- View mode - show display values -->
|
||||
<template v-else>
|
||||
<div v-if="getDescriptor(key)">
|
||||
<span v-if="getDescriptor(key)?.propertyType === 'Boolean'">
|
||||
<v-icon
|
||||
:icon="modelValue[key] ? 'mdi-check' : 'mdi-close'"
|
||||
:color="modelValue[key] ? 'success' : 'error'"
|
||||
/>
|
||||
</span>
|
||||
<span v-else-if="getDescriptor(key)?.propertyType === 'JSON'">
|
||||
<div class="json-display" style="max-height: 100px; overflow-y: auto">
|
||||
{{ formatJsonDisplay(modelValue[key]) }}
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ formatValueDisplay(key) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ formatValueDisplay(key) }}
|
||||
</div>
|
||||
</template>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<v-btn
|
||||
v-if="!disabled"
|
||||
icon="mdi-close"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="removeProperty(key)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Add new property row -->
|
||||
<tr v-if="!disabled">
|
||||
<td class="align-middle">
|
||||
<v-combobox
|
||||
v-model="propertyInput"
|
||||
:items="availableForCombobox"
|
||||
:error-messages="propertyInputError"
|
||||
item-title="displayName"
|
||||
item-value="name"
|
||||
placeholder="Имя нового свойства"
|
||||
hide-details
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
@update:model-value="onPropertyInputChange"
|
||||
/>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!getInputValue().trim() || !!propertyInputError"
|
||||
size="small"
|
||||
@click="addPropertyFromInput"
|
||||
>
|
||||
Добавить
|
||||
</v-btn>
|
||||
</td>
|
||||
<td class="text-center align-middle">
|
||||
<!-- Empty cell for actions column -->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
<v-alert v-if="isLoading" type="info" variant="tonal" class="mt-4">
|
||||
Загрузка дескрипторов свойств...
|
||||
</v-alert>
|
||||
|
||||
<v-alert v-if="error" type="error" variant="tonal" class="mt-4">
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import type {
|
||||
PropertyDescriptor,
|
||||
NumberConstraint,
|
||||
ValueListConstraint,
|
||||
StringConstraint
|
||||
} from '@/generated/RpcClient'
|
||||
import PropertyWidget from '@/components/PropertyWidget.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const propertyForm = ref()
|
||||
const isFormValid = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const descriptors = ref<PropertyDescriptor[]>([])
|
||||
const propertyInput = ref<string>('')
|
||||
const propertyInputError = ref<string>('')
|
||||
|
||||
const hasPropertyInputError = computed(() => {
|
||||
return !!propertyInputError.value
|
||||
})
|
||||
|
||||
const availableForCombobox = computed(() => {
|
||||
const existingKeys = Object.keys(props.modelValue || {})
|
||||
return availableDescriptors.value.filter((d) => !existingKeys.includes(d.name))
|
||||
})
|
||||
|
||||
const buttonDisabled = computed(() => {
|
||||
const hasText = propertyInputText.value && propertyInputText.value.trim().length > 0
|
||||
const hasError = !!propertyInputError.value && propertyInputError.value.trim().length > 0
|
||||
|
||||
console.log('Debug button:', {
|
||||
text: propertyInputText.value,
|
||||
hasText: hasText,
|
||||
error: propertyInputError.value,
|
||||
hasError: hasError,
|
||||
disabled: !hasText || hasError
|
||||
})
|
||||
|
||||
return !hasText || hasError
|
||||
})
|
||||
|
||||
const getDescriptor = (key: string): PropertyDescriptor | undefined => {
|
||||
return descriptors.value.find((d) => d.name === key)
|
||||
}
|
||||
|
||||
const availableDescriptors = computed(() => {
|
||||
const existingKeys = Object.keys(props.modelValue || {})
|
||||
return descriptors.value
|
||||
.filter((d) => !existingKeys.includes(d.name))
|
||||
.map((d) => ({
|
||||
...d,
|
||||
displayName: d.name
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
})
|
||||
|
||||
const getInputValue = (): string => {
|
||||
if (typeof propertyInput.value === 'object' && propertyInput.value?.name) {
|
||||
return propertyInput.value.name
|
||||
}
|
||||
return String(propertyInput.value || '')
|
||||
}
|
||||
|
||||
const onPropertyInputChange = (newValue: any) => {
|
||||
propertyInput.value = newValue
|
||||
validateInput(getInputValue())
|
||||
}
|
||||
|
||||
const validateInput = (input: string) => {
|
||||
const existingKeys = Object.keys(props.modelValue || {})
|
||||
const trimmedName = input.trim()
|
||||
|
||||
if (!trimmedName) {
|
||||
propertyInputError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (existingKeys.includes(trimmedName)) {
|
||||
propertyInputError.value = 'Свойство с таким именем уже существует'
|
||||
} else {
|
||||
propertyInputError.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Watch input for validation
|
||||
watch(
|
||||
() => getInputValue(),
|
||||
(newValue) => {
|
||||
validateInput(newValue)
|
||||
}
|
||||
)
|
||||
|
||||
const updateValue = (key: string, newValue: any) => {
|
||||
const updatedValues = { ...props.modelValue }
|
||||
updatedValues[key] = newValue
|
||||
emit('update:modelValue', updatedValues)
|
||||
}
|
||||
|
||||
const addPropertyFromInput = () => {
|
||||
const propertyName = getInputValue().trim()
|
||||
if (!propertyName || propertyInputError.value) return
|
||||
|
||||
const existingKeys = Object.keys(props.modelValue || {})
|
||||
if (existingKeys.includes(propertyName)) return
|
||||
|
||||
const descriptor = getDescriptor(propertyName)
|
||||
let defaultValue: any = ''
|
||||
|
||||
if (descriptor) {
|
||||
// Set default values based on property type
|
||||
switch (descriptor.propertyType) {
|
||||
case 'Boolean':
|
||||
defaultValue = false
|
||||
break
|
||||
case 'Number':
|
||||
const numberConstraint = descriptor.constraints?.find((c) => c.type === 'NumberConstraint')
|
||||
if (numberConstraint) {
|
||||
defaultValue = numberConstraint.min || 0
|
||||
} else {
|
||||
defaultValue = 0
|
||||
}
|
||||
break
|
||||
case 'ValueList':
|
||||
const valueListConstraint = descriptor.constraints?.find(
|
||||
(c) => c.type === 'ValueListConstraint'
|
||||
)
|
||||
if (valueListConstraint && valueListConstraint.values?.length > 0) {
|
||||
defaultValue = valueListConstraint.values[0]
|
||||
} else {
|
||||
defaultValue = ''
|
||||
}
|
||||
break
|
||||
case 'JSON':
|
||||
defaultValue = {}
|
||||
break
|
||||
case 'Text':
|
||||
default:
|
||||
defaultValue = ''
|
||||
break
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (descriptor.array) {
|
||||
defaultValue = [defaultValue]
|
||||
}
|
||||
} else {
|
||||
// Custom property - empty string
|
||||
defaultValue = ''
|
||||
}
|
||||
|
||||
const updatedValues = { ...props.modelValue }
|
||||
updatedValues[propertyName] = defaultValue
|
||||
emit('update:modelValue', updatedValues)
|
||||
|
||||
// Reset input
|
||||
propertyInput.value = ''
|
||||
}
|
||||
|
||||
const removeProperty = (key: string) => {
|
||||
const updatedValues = { ...props.modelValue }
|
||||
delete updatedValues[key]
|
||||
emit('update:modelValue', updatedValues)
|
||||
}
|
||||
|
||||
const formatValueDisplay = (key: string): string => {
|
||||
const value = props.modelValue[key]
|
||||
const descriptor = getDescriptor(key)
|
||||
|
||||
if (descriptor?.propertyType === 'ValueList') {
|
||||
const valueListConstraint = descriptor.constraints?.find(
|
||||
(c) => c.type === 'ValueListConstraint'
|
||||
)
|
||||
const option = valueListConstraint?.values?.find((v: any) => v.value === value)
|
||||
return option?.label || value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.length} элементов]`
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return '[Объект]'
|
||||
}
|
||||
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const formatJsonDisplay = (value: any): string => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2)
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const loadDescriptors = async () => {
|
||||
// Import app store dynamically to avoid circular dependency
|
||||
const { useAppStore } = await import('@/stores/app')
|
||||
const appStore = useAppStore()
|
||||
|
||||
if (!appStore.api) {
|
||||
error.value = 'API недоступен'
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
descriptors.value = await appStore.api.Properties.getAll()
|
||||
} catch (err) {
|
||||
console.error('Error loading property descriptors:', err)
|
||||
error.value = 'Не удалось загрузить дескрипторы свойств'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDescriptors()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.property-editor {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.v-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
<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>
|
||||
|
|
@ -128,34 +128,10 @@
|
|||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Карта с дополнительной информацией -->
|
||||
<!-- Редактор свойств пользователя -->
|
||||
<v-card elevation="2" class="mt-4">
|
||||
<v-card-text>
|
||||
<v-list density="compact">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-fingerprint</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>ID пользователя</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ currentUser?.id }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-identifier</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>UUID</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ currentUser?.uuid }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-shield-account</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Роль</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ userRole }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<property-editor v-model="userValues" :disabled="!isEditing" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
|
@ -168,6 +144,7 @@
|
|||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
import PropertyEditor from '@/components/PropertyEditor.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { showSuccess, showError } = useNotificationStore()
|
||||
|
|
@ -199,6 +176,9 @@ const profileData = ref({
|
|||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// Свойства пользователя для редактора
|
||||
const userValues = ref<Record<string, any>>({})
|
||||
|
||||
const originalData = ref({
|
||||
login: '',
|
||||
name: '',
|
||||
|
|
@ -227,7 +207,8 @@ const passwordRules = [
|
|||
]
|
||||
|
||||
const confirmPasswordRules = [
|
||||
(v: string) => !profileData.newPassword || v === profileData.newPassword || 'Пароли не совпадают'
|
||||
(v: string) =>
|
||||
!profileData.value.newPassword || v === profileData.value.newPassword || 'Пароли не совпадают'
|
||||
]
|
||||
|
||||
const getUserDisplayName = () => {
|
||||
|
|
@ -239,10 +220,7 @@ const getUserDisplayName = () => {
|
|||
}
|
||||
|
||||
const startEditing = () => {
|
||||
originalData.value = {
|
||||
login: profileData.value.login,
|
||||
name: profileData.value.name
|
||||
}
|
||||
originalData.value = { ...profileData.value }
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
|
|
@ -272,7 +250,7 @@ const saveProfile = async () => {
|
|||
const success = await appStore.api.Profile.save(
|
||||
profileData.value.login,
|
||||
profileData.value.name,
|
||||
currentUser.value?.values || {},
|
||||
userValues.value,
|
||||
password,
|
||||
profileData.value.currentPassword
|
||||
)
|
||||
|
|
@ -312,6 +290,7 @@ const loadProfileData = () => {
|
|||
confirmPassword: ''
|
||||
}
|
||||
originalData.value = { ...profileData.value }
|
||||
userValues.value = user.values || {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue