mirror of
https://github.com/OpenBMB/ChatDev.git
synced 2026-04-25 11:18:06 +00:00
1605 lines
43 KiB
Vue
Executable File
1605 lines
43 KiB
Vue
Executable File
<template>
|
||
<div class="form-field" v-show="isVisible">
|
||
<template v-if="field.childNode">
|
||
<div v-if="recursive" class="form-group form-group-inline child-node-group">
|
||
<label>
|
||
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
|
||
<span v-if="field.required" class="required-asterisk">*</span>
|
||
<RichTooltip
|
||
v-if="field.description"
|
||
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
|
||
placement="top"
|
||
>
|
||
<span class="help-icon" tabindex="0">?</span>
|
||
</RichTooltip>
|
||
</label>
|
||
<div class="child-node-container">
|
||
<div class="child-node-controls">
|
||
<button @click="$emit('open-child-modal', field)" class="add-child-button">
|
||
<span class="plus-icon" aria-hidden="true">
|
||
<!-- Edit icon when configured -->
|
||
<svg
|
||
v-if="!isList && hasChildValue"
|
||
width="16"
|
||
height="16"
|
||
viewBox="0 0 16 16"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<rect
|
||
x="3"
|
||
y="2"
|
||
width="8"
|
||
height="11"
|
||
rx="1"
|
||
ry="1"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.2"
|
||
/>
|
||
<path
|
||
fill="currentColor"
|
||
d="M10.9 3.1a.9.9 0 0 1 1.27 0l.73.73a.9.9 0 0 1 0 1.27l-4.3 4.3-1.7.5.5-1.7 4.3-4.3z"
|
||
/>
|
||
</svg>
|
||
<!-- Configure icon when not yet configured -->
|
||
<svg
|
||
v-else
|
||
width="16"
|
||
height="16"
|
||
viewBox="0 0 16 16"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<path
|
||
fill="currentColor"
|
||
d="M8 2.5a.75.75 0 0 1 .75.75v4h4a.75.75 0 0 1 0 1.5h-4v4a.75.75 0 0 1-1.5 0v-4h-4a.75.75 0 0 1 0-1.5h4v-4A.75.75 0 0 1 8 2.5z"
|
||
/>
|
||
</svg>
|
||
</span>
|
||
{{ childNodeButtonLabel }}
|
||
</button>
|
||
<!-- Show clear button only when child is already configured ("Edit" mode) -->
|
||
<button
|
||
v-if="hasChildValue"
|
||
class="clear-child-button"
|
||
@click.stop="$emit('clear-child-entry', field.name)"
|
||
:title="$t('dynamic_form_field.clear_configuration')"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div
|
||
v-if="isList && formData[field.name] && formData[field.name].length"
|
||
class="child-node-list"
|
||
>
|
||
<div
|
||
v-for="(childEntry, childIndex) in formData[field.name]"
|
||
:key="childIndex"
|
||
class="child-node-item-wrapper"
|
||
>
|
||
<div
|
||
class="child-node-item"
|
||
@click="$emit('open-child-modal', field, childIndex)"
|
||
>
|
||
<span class="child-node-name">
|
||
{{ describeChildEntry(childEntry, childIndex) }}
|
||
</span>
|
||
<div class="child-node-actions">
|
||
<button
|
||
class="delete-child-button"
|
||
@click.stop="$emit('delete-child-entry', field.name, childIndex)"
|
||
:title="$t('dynamic_form_field.delete_child')"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="childSummaries[field.name] && childSummaries[field.name][childIndex]"
|
||
class="child-node-summary"
|
||
>
|
||
{{ childSummaries[field.name][childIndex] }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-else-if="!isList && hasChildValue && childSummaries[field.name]"
|
||
class="child-node-summary"
|
||
>
|
||
{{ childSummaries[field.name] }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="hasChildRoutes">
|
||
<!-- Logic for expanded inline config will go here in FormGenerator usage or via prop controls -->
|
||
<!-- But for now, we implement standard behavior. The expanding logic will be handled by the parent using slots or a different prop -->
|
||
|
||
<!-- Standard Popup Style (if not expanded) -->
|
||
<div v-if="!expandInline" class="form-group form-group-inline child-node-group">
|
||
<label>
|
||
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
|
||
<span v-if="field.required" class="required-asterisk">*</span>
|
||
<RichTooltip
|
||
v-if="field.description"
|
||
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
|
||
placement="top"
|
||
>
|
||
<span class="help-icon" tabindex="0">?</span>
|
||
</RichTooltip>
|
||
</label>
|
||
<div class="child-node-container">
|
||
<div class="child-node-controls">
|
||
<button
|
||
@click="$emit('open-conditional-child-modal', field)"
|
||
class="add-child-button"
|
||
:disabled="!canOpenConditionalChildModal"
|
||
>
|
||
<span class="plus-icon" aria-hidden="true">
|
||
<!-- Edit icon when a child config already exists -->
|
||
<svg
|
||
v-if="formData[field.name]"
|
||
width="16"
|
||
height="16"
|
||
viewBox="0 0 16 16"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<rect
|
||
x="3"
|
||
y="2"
|
||
width="8"
|
||
height="11"
|
||
rx="1"
|
||
ry="1"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.2"
|
||
/>
|
||
<path
|
||
fill="currentColor"
|
||
d="M10.9 3.1a.9.9 0 0 1 1.27 0l.73.73a.9.9 0 0 1 0 1.27l-4.3 4.3-1.7.5.5-1.7 4.3-4.3z"
|
||
/>
|
||
</svg>
|
||
<!-- Configure/Add icon when not yet configured -->
|
||
<svg
|
||
v-else
|
||
width="16"
|
||
height="16"
|
||
viewBox="0 0 16 16"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<path
|
||
fill="currentColor"
|
||
d="M8 2.5a.75.75 0 0 1 .75.75v4h4a.75.75 0 0 1 0 1.5h-4v4a.75.75 0 0 1-1.5 0v-4h-4a.75.75 0 0 1 0-1.5h4v-4A.75.75 0 0 1 8 2.5z"
|
||
/>
|
||
</svg>
|
||
</span>
|
||
{{ conditionalChildButtonLabel }}
|
||
</button>
|
||
<button
|
||
v-if="hasChildValue"
|
||
class="clear-child-button"
|
||
@click.stop="$emit('clear-child-entry', field.name)"
|
||
title="Clear configuration"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div
|
||
v-if="!canOpenConditionalChildModal"
|
||
class="child-node-hint"
|
||
>
|
||
{{ $t('dynamic_form_field.select_type_and_configure') }}
|
||
</div>
|
||
<div
|
||
v-if="hasChildValue && childSummaries[field.name]"
|
||
class="child-node-summary"
|
||
>
|
||
{{ childSummaries[field.name] }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Expanded Inline Style is NOT rendered here. The parent will check `expandInline`
|
||
and simply NOT render this button-based UI, and instead render the inline form.
|
||
Wait, if I am extracting this, I should support both or let parent handle the expanded part adjacent to this component?
|
||
The requirement says: "display type selection" (which is a standard field),
|
||
then "display expanded config below".
|
||
So the "config" field (which is `hasChildRoutes`) should be rendered DIFFERENTLY if expanded.
|
||
If it's expanded, we might just want to render NOTHING for this field here,
|
||
and let the parent render the separator + sub-form?
|
||
OR we render the separator + sub-form HERE?
|
||
But complex recursion logic is easier in parent.
|
||
|
||
DECISION: If `expandInline` is true, this component renders NOTHING for the `childRoutes` field,
|
||
because the content is rendered by the parent (the "separator" + "inline fields").
|
||
Actually, the `type` field is a separate field. This component renders it.
|
||
The `config` field IS the field with `childRoutes`.
|
||
So if `expandInline` is true, we render NOTHING?
|
||
Yes. The parent will detect this field and render the specialized inline UI instead.
|
||
|
||
However, I need to make sure I don't break existing logic.
|
||
If I pass `expandInline` prop:
|
||
-->
|
||
<div v-else-if="expandInline">
|
||
<!-- Render nothing, parent handles it -->
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<!-- Choice dropdown fields-->
|
||
<div v-if="field.enum && !isList" class="form-group">
|
||
<label :for="`${modalId}-${field.name}`">
|
||
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
|
||
<span v-if="field.required" class="required-asterisk">*</span>
|
||
<RichTooltip
|
||
v-if="field.description"
|
||
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
|
||
placement="top"
|
||
>
|
||
<span class="help-icon" tabindex="0">?</span>
|
||
</RichTooltip>
|
||
</label>
|
||
<div ref="selectWrapper" class="custom-select-wrapper" :class="{ 'select-disabled': isReadOnly }" @click="toggleDropdown">
|
||
<input
|
||
:id="`${modalId}-${field.name}`"
|
||
:value="inputValue"
|
||
class="form-input custom-select-input"
|
||
readonly
|
||
:class="{'input-readonly': isReadOnly}"
|
||
:placeholder="$t('dynamic_form_field.type_to_filter')"
|
||
autocomplete="off"
|
||
/>
|
||
<Teleport to="body">
|
||
<div v-if="showDropdown && !isReadOnly" class="custom-select-dropdown" :style="dropdownStyle">
|
||
<div
|
||
v-if="!field.required"
|
||
class="custom-select-option"
|
||
:class="{ 'option-selected': formData[field.name] === null }"
|
||
@mousedown.stop="selectOption(null)"
|
||
>
|
||
None
|
||
</div>
|
||
<div
|
||
v-for="option in filteredOptions"
|
||
:key="typeof option === 'string' ? option : option.value"
|
||
class="custom-select-option"
|
||
:class="{ 'option-selected': formData[field.name] === (typeof option === 'string' ? option : option.value) }"
|
||
:title="typeof option === 'string' ? '' : (option.description || '')"
|
||
@mousedown.stop="selectOption(typeof option === 'string' ? option : option.value)"
|
||
>
|
||
{{
|
||
typeof option === 'string'
|
||
? option
|
||
: (option.label || option.value)
|
||
}}
|
||
</div>
|
||
<div v-if="filteredOptions.length === 0" class="custom-select-no-results">
|
||
{{ $t('dynamic_form_field.no_options_found') }}
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
<div class="custom-select-arrow" :class="{ 'arrow-open': showDropdown }">
|
||
▼
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Multi-choice fields -->
|
||
<div v-else-if="field.enum && isList" class="form-group">
|
||
<label>
|
||
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
|
||
<span v-if="field.required" class="required-asterisk">*</span>
|
||
<RichTooltip
|
||
v-if="field.description"
|
||
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
|
||
placement="top"
|
||
>
|
||
<span class="help-icon" tabindex="0">?</span>
|
||
</RichTooltip>
|
||
</label>
|
||
<div class="multi-select-options">
|
||
<label
|
||
v-for="option in (field.enumOptions || field.enum)"
|
||
:key="typeof option === 'string' ? option : option.value"
|
||
class="multi-select-option"
|
||
:title="typeof option === 'string' ? '' : (option.description || '')"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
:value="typeof option === 'string' ? option : option.value"
|
||
v-model="internalFormData[field.name]"
|
||
/>
|
||
<span class="option-label">
|
||
{{
|
||
typeof option === 'string'
|
||
? option
|
||
: (option.label || option.value)
|
||
}}
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Boolean switch fields -->
|
||
<div v-else-if="field.type === 'bool' && !isList" class="form-group">
|
||
<div class="switch-wrapper">
|
||
<label :for="`${modalId}-${field.name}`" class="switch-label-text">
|
||
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
|
||
<span v-if="field.required" class="required-asterisk">*</span>
|
||
<RichTooltip
|
||
v-if="field.description"
|
||
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
|
||
placement="top"
|
||
>
|
||
<span class="help-icon" tabindex="0">?</span>
|
||
</RichTooltip>
|
||
</label>
|
||
<label class="switch-container">
|
||
<input
|
||
type="checkbox"
|
||
:id="`${modalId}-${field.name}`"
|
||
:checked="formData[field.name] === true"
|
||
@change="onBooleanSwitchChange($event.target.checked)"
|
||
:disabled="isReadOnly"
|
||
/>
|
||
<span class="switch-slider"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- String input fields -->
|
||
<div v-else-if="field.type === 'str' && !isList" class="form-group">
|
||
<label :for="`${modalId}-${field.name}`">
|
||
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
|
||
<span v-if="field.required" class="required-asterisk">*</span>
|
||
<RichTooltip
|
||
v-if="field.description"
|
||
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
|
||
placement="top"
|
||
>
|
||
<span class="help-icon" tabindex="0">?</span>
|
||
</RichTooltip>
|
||
</label>
|
||
<input
|
||
:id="`${modalId}-${field.name}`"
|
||
:value="formData[field.name]"
|
||
@input="onInput($event.target.value)"
|
||
type="text"
|
||
class="form-input"
|
||
:readonly="isReadOnly"
|
||
:class="{'input-readonly': isReadOnly}"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Multiline text fields -->
|
||
<div v-else-if="field.type === 'text' && !isList" class="form-group">
|
||
<label :for="`${modalId}-${field.name}`">
|
||
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
|
||
<span v-if="field.required" class="required-asterisk">*</span>
|
||
<RichTooltip
|
||
v-if="field.description"
|
||
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
|
||
placement="top"
|
||
>
|
||
<span class="help-icon" tabindex="0">?</span>
|
||
</RichTooltip>
|
||
</label>
|
||
<textarea
|
||
:id="`${modalId}-${field.name}`"
|
||
:value="formData[field.name]"
|
||
@input="onInput($event.target.value)"
|
||
class="form-textarea"
|
||
rows="4"
|
||
:readonly="isReadOnly"
|
||
:class="{'input-readonly': isReadOnly}"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Integer input fields -->
|
||
<div v-else-if="field.type === 'int' && !isList" class="form-group">
|
||
<label :for="`${modalId}-${field.name}`">
|
||
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
|
||
<span v-if="field.required" class="required-asterisk">*</span>
|
||
<RichTooltip
|
||
v-if="field.description"
|
||
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
|
||
placement="top"
|
||
>
|
||
<span class="help-icon" tabindex="0">?</span>
|
||
</RichTooltip>
|
||
</label>
|
||
<input
|
||
:id="`${modalId}-${field.name}`"
|
||
:value="formData[field.name]"
|
||
@input="onInputNumber($event.target.value)"
|
||
type="number"
|
||
step="1"
|
||
class="form-input"
|
||
:readonly="isReadOnly"
|
||
:class="{'input-readonly': isReadOnly}"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Float input fields -->
|
||
<div v-else-if="field.type === 'float' && !isList" class="form-group">
|
||
<label :for="`${modalId}-${field.name}`">
|
||
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
|
||
<span v-if="field.required" class="required-asterisk">*</span>
|
||
<RichTooltip
|
||
v-if="field.description"
|
||
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
|
||
placement="top"
|
||
>
|
||
<span class="help-icon" tabindex="0">?</span>
|
||
</RichTooltip>
|
||
</label>
|
||
<input
|
||
:id="`${modalId}-${field.name}`"
|
||
:value="formData[field.name]"
|
||
@input="onInputNumber($event.target.value)"
|
||
type="number"
|
||
step="any"
|
||
class="form-input"
|
||
:readonly="isReadOnly"
|
||
:class="{'input-readonly': isReadOnly}"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Key-value fields -->
|
||
<div v-else-if="field.type === 'dict[str, Any]' || field.type === 'dict[str, str]'" class="form-group form-group-inline">
|
||
<label>
|
||
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
|
||
<span v-if="field.required" class="required-asterisk">*</span>
|
||
<RichTooltip
|
||
v-if="field.description"
|
||
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
|
||
placement="top"
|
||
>
|
||
<span class="help-icon" tabindex="0">?</span>
|
||
</RichTooltip>
|
||
</label>
|
||
<div class="vars-container">
|
||
<button @click="$emit('open-var-modal', field.name)" class="add-var-button">
|
||
<span class="plus-icon" aria-hidden="true">
|
||
<svg
|
||
width="16"
|
||
height="16"
|
||
viewBox="0 0 16 16"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<path
|
||
fill="currentColor"
|
||
d="M8 2.5a.75.75 0 0 1 .75.75v4h4a.75.75 0 0 1 0 1.5h-4v4a.75.75 0 0 1-1.5 0v-4h-4a.75.75 0 0 1 0-1.5h4v-4A.75.75 0 0 1 8 2.5z"
|
||
/>
|
||
</svg>
|
||
</span>
|
||
{{ $t('dynamic_form_field.add_key_value') }}
|
||
</button>
|
||
<div
|
||
v-if="formData[field.name] && hasDictEntries(formData[field.name])"
|
||
class="vars-list"
|
||
>
|
||
<div
|
||
v-for="(varValue, varKey) in formData[field.name]"
|
||
:key="varKey"
|
||
class="var-item"
|
||
@click="$emit('edit-var', field.name, varKey)"
|
||
>
|
||
<span class="var-name">{{ varKey }}</span>
|
||
<span class="var-separator">|</span>
|
||
<span class="var-value">{{ varValue }}</span>
|
||
<button
|
||
class="delete-var-button"
|
||
@click.stop="$emit('delete-var', field.name, varKey)"
|
||
:title="$t('dynamic_form_field.delete_variable')"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- List of strings field -->
|
||
<div v-else-if="isList && field.type.includes('str')" class="form-group form-group-inline">
|
||
<label>
|
||
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
|
||
<span v-if="field.required" class="required-asterisk">*</span>
|
||
<RichTooltip
|
||
v-if="field.description"
|
||
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
|
||
placement="top"
|
||
>
|
||
<span class="help-icon" tabindex="0">?</span>
|
||
</RichTooltip>
|
||
</label>
|
||
<div class="list-container">
|
||
<button @click="$emit('open-list-item-modal', field.name)" class="add-list-button">
|
||
<span class="plus-icon" aria-hidden="true">
|
||
<svg
|
||
width="16"
|
||
height="16"
|
||
viewBox="0 0 16 16"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<path
|
||
fill="currentColor"
|
||
d="M8 2.5a.75.75 0 0 1 .75.75v4h4a.75.75 0 0 1 0 1.5h-4v4a.75.75 0 0 1-1.5 0v-4h-4a.75.75 0 0 1 0-1.5h4v-4A.75.75 0 0 1 8 2.5z"
|
||
/>
|
||
</svg>
|
||
</span>
|
||
Add Entry
|
||
</button>
|
||
<div v-if="formData[field.name] && formData[field.name].length > 0" class="list-items">
|
||
<div
|
||
v-for="(item, index) in formData[field.name]"
|
||
:key="index"
|
||
class="list-item"
|
||
>
|
||
<span class="item-value">{{ item }}</span>
|
||
<button
|
||
class="delete-item-button"
|
||
@click.stop="$emit('delete-list-item', field.name, index)"
|
||
:title="$t('dynamic_form_field.delete_item')"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref, reactive, watch, onUnmounted } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import RichTooltip from './RichTooltip.vue'
|
||
|
||
const { t } = useI18n()
|
||
|
||
const props = defineProps({
|
||
field: {
|
||
type: Object,
|
||
required: true
|
||
},
|
||
modalId: {
|
||
type: String,
|
||
required: true
|
||
},
|
||
formData: {
|
||
type: Object,
|
||
required: true
|
||
},
|
||
childSummaries: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
recursive: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
isVisible: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// If true, and this field is a childRoute config, render nothing (handled by parent inline)
|
||
expandInline: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// Helpers passed from parent or reimplemented
|
||
canOpenConditionalChildModal: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
conditionalChildButtonLabel: {
|
||
type: String,
|
||
default: 'Configure'
|
||
},
|
||
activeChildRoute: {
|
||
type: Object,
|
||
default: null
|
||
},
|
||
isReadOnly: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
})
|
||
|
||
const isReadOnly = computed(() => props.isReadOnly)
|
||
|
||
const emit = defineEmits([
|
||
'update:form-data',
|
||
'open-child-modal',
|
||
'clear-child-entry',
|
||
'delete-child-entry',
|
||
'open-conditional-child-modal',
|
||
'handle-enum-change',
|
||
'open-var-modal',
|
||
'edit-var',
|
||
'delete-var',
|
||
'open-list-item-modal',
|
||
'delete-list-item'
|
||
])
|
||
|
||
// Computed property to allow v-model binding with mutation of prop object (common in complex forms)
|
||
// OR better, just use props.formData[field.name] directly as Vue 3 proxies allow it, though strict mode warns.
|
||
// For FormGenerator, we generally mutate the reactive modal object.
|
||
const internalFormData = computed(() => props.formData)
|
||
|
||
// Custom select filtering state
|
||
const showDropdown = ref(false)
|
||
const filterText = ref('')
|
||
const selectWrapper = ref(null)
|
||
const dropdownRect = reactive({
|
||
top: 0,
|
||
left: 0,
|
||
width: 0
|
||
})
|
||
|
||
const updateDropdownPosition = () => {
|
||
if (selectWrapper.value) {
|
||
const rect = selectWrapper.value.getBoundingClientRect()
|
||
dropdownRect.top = rect.bottom
|
||
dropdownRect.left = rect.left
|
||
dropdownRect.width = rect.width
|
||
}
|
||
}
|
||
|
||
const dropdownStyle = computed(() => ({
|
||
position: 'fixed',
|
||
top: `${dropdownRect.top + 4}px`,
|
||
left: `${dropdownRect.left}px`,
|
||
width: `${dropdownRect.width}px`,
|
||
zIndex: 10000
|
||
}))
|
||
|
||
const toggleDropdown = () => {
|
||
if (props.isReadOnly) return
|
||
if (!showDropdown.value) {
|
||
updateDropdownPosition()
|
||
showDropdown.value = true
|
||
// Add escape listener
|
||
window.addEventListener('scroll', closeOnScroll, true)
|
||
window.addEventListener('mousedown', handleOutsideClick)
|
||
} else {
|
||
closeDropdown()
|
||
}
|
||
}
|
||
|
||
const closeDropdown = () => {
|
||
showDropdown.value = false
|
||
window.removeEventListener('scroll', closeOnScroll, true)
|
||
window.removeEventListener('mousedown', handleOutsideClick)
|
||
}
|
||
|
||
const closeOnScroll = () => {
|
||
if (showDropdown.value) closeDropdown()
|
||
}
|
||
|
||
const handleOutsideClick = (e) => {
|
||
if (selectWrapper.value && !selectWrapper.value.contains(e.target)) {
|
||
closeDropdown()
|
||
}
|
||
}
|
||
|
||
const isList = computed(() => {
|
||
return props.field.type && props.field.type.includes('list[')
|
||
})
|
||
|
||
const hasChildRoutes = computed(() => {
|
||
return Array.isArray(props.field?.childRoutes) && props.field.childRoutes.length > 0
|
||
})
|
||
|
||
const hasChildValue = computed(() => {
|
||
const value = props.formData?.[props.field.name]
|
||
if (Array.isArray(value)) {
|
||
return value.length > 0
|
||
}
|
||
return value !== null && value !== undefined && value !== ''
|
||
})
|
||
|
||
const childNodeButtonLabel = computed(() => {
|
||
if (isList.value) {
|
||
return t('dynamic_form_field.add_entry')
|
||
}
|
||
return props.formData[props.field.name] ? t('dynamic_form_field.edit_child', { child: props.field.childNode }) : t('dynamic_form_field.configure_child', { child: props.field.childNode })
|
||
})
|
||
|
||
// Filtered options for custom select
|
||
const filteredOptions = computed(() => {
|
||
const options = props.field.enumOptions || props.field.enum || []
|
||
if (!filterText.value.trim()) {
|
||
return options
|
||
}
|
||
const filter = filterText.value.toLowerCase()
|
||
return options.filter(option => {
|
||
const label = typeof option === 'string' ? option : (option.label || option.value)
|
||
return label.toLowerCase().includes(filter)
|
||
})
|
||
})
|
||
|
||
// Computed input value for custom select
|
||
const inputValue = computed(() => {
|
||
if (showDropdown.value) {
|
||
return filterText.value
|
||
}
|
||
return getSelectedLabel()
|
||
})
|
||
|
||
// Helpers
|
||
const hasDictEntries = (value) => {
|
||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||
return false
|
||
}
|
||
return Object.keys(value).length > 0
|
||
}
|
||
|
||
const describeChildEntry = (entry, index) => {
|
||
if (entry && typeof entry === 'object') {
|
||
if (entry.id) return entry.id
|
||
if (entry.name) return entry.name
|
||
if (entry.type) return entry.type
|
||
}
|
||
return `${props.field.childNode} #${index + 1}`
|
||
}
|
||
|
||
// Input handlers
|
||
const onInput = (value) => {
|
||
props.formData[props.field.name] = value
|
||
}
|
||
|
||
const onInputNumber = (value) => {
|
||
// Check if empty string
|
||
if (value === "") {
|
||
props.formData[props.field.name] = null
|
||
return
|
||
}
|
||
props.formData[props.field.name] = Number(value)
|
||
}
|
||
|
||
const onBooleanSwitchChange = (checked) => {
|
||
props.formData[props.field.name] = checked
|
||
}
|
||
|
||
// Custom select methods
|
||
const onFilterInput = (event) => {
|
||
filterText.value = event.target.value
|
||
showDropdown.value = true
|
||
}
|
||
|
||
const handleInputBlur = () => {
|
||
// Delay hiding dropdown to allow option selection
|
||
setTimeout(() => {
|
||
showDropdown.value = false
|
||
filterText.value = ''
|
||
}, 200)
|
||
}
|
||
|
||
const selectOption = (value) => {
|
||
props.formData[props.field.name] = value
|
||
closeDropdown()
|
||
emit('handle-enum-change', props.field)
|
||
}
|
||
|
||
const getSelectedLabel = () => {
|
||
const currentValue = props.formData[props.field.name]
|
||
if (currentValue === null || currentValue === undefined) {
|
||
return ''
|
||
}
|
||
|
||
const options = props.field.enumOptions || props.field.enum || []
|
||
const selectedOption = options.find(option => {
|
||
const optionValue = typeof option === 'string' ? option : option.value
|
||
return optionValue === currentValue
|
||
})
|
||
|
||
if (selectedOption) {
|
||
return typeof selectedOption === 'string'
|
||
? selectedOption
|
||
: (selectedOption.label || selectedOption.value)
|
||
}
|
||
|
||
return currentValue
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.form-field {
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
color: #f2f2f2;
|
||
font-weight: 500;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.form-group-inline {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 14px;
|
||
}
|
||
|
||
.form-group-inline label {
|
||
margin-bottom: 0;
|
||
flex-shrink: 0;
|
||
min-width: 96px;
|
||
}
|
||
|
||
.form-group-inline > *:not(label) {
|
||
flex: 1;
|
||
}
|
||
|
||
.required-asterisk {
|
||
color: #ff6b6b;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.help-icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 14px;
|
||
height: 14px;
|
||
margin-left: 6px;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||
font-size: 11px;
|
||
line-height: 1;
|
||
cursor: default;
|
||
color: rgba(242, 242, 242, 0.7);
|
||
background-color: transparent;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.help-icon:hover {
|
||
border-color: #a0c4ff;
|
||
color: #a0c4ff;
|
||
}
|
||
|
||
.form-input,
|
||
.form-select,
|
||
.form-textarea,
|
||
.multi-select-options input[type='checkbox'] {
|
||
font-family: 'Inter', sans-serif;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
padding: 9px 11px;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
background-color: rgba(10, 10, 10, 0.6);
|
||
color: #f2f2f2;
|
||
font-size: 13px;
|
||
box-sizing: border-box;
|
||
transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.form-textarea {
|
||
width: 100%;
|
||
padding: 9px 11px;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
background-color: rgba(10, 10, 10, 0.6);
|
||
color: #f2f2f2;
|
||
font-size: 13px;
|
||
box-sizing: border-box;
|
||
min-height: 80px;
|
||
resize: vertical;
|
||
transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.form-textarea:focus {
|
||
outline: none;
|
||
border-color: #a0c4ff;
|
||
background-color: rgba(15, 15, 15, 0.85);
|
||
box-shadow: 0 0 0 1px rgba(160, 196, 255, 0.5);
|
||
}
|
||
|
||
.form-input:focus {
|
||
outline: none;
|
||
border-color: #a0c4ff;
|
||
background-color: rgba(15, 15, 15, 0.85);
|
||
box-shadow: 0 0 0 1px rgba(160, 196, 255, 0.5);
|
||
}
|
||
|
||
.form-input::placeholder {
|
||
color: rgba(255, 255, 255, 0.35);
|
||
}
|
||
|
||
.form-select {
|
||
width: 100%;
|
||
padding: 9px 11px;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
background-color: rgba(10, 10, 10, 0.6);
|
||
color: #f2f2f2;
|
||
font-size: 13px;
|
||
box-sizing: border-box;
|
||
cursor: pointer;
|
||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||
}
|
||
|
||
.form-select:focus {
|
||
outline: none;
|
||
border-color: #a0c4ff;
|
||
background-color: rgba(15, 15, 15, 0.85);
|
||
}
|
||
|
||
/* Switch Styles */
|
||
.switch-wrapper {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
min-height: 38px; /* Match input height roughly */
|
||
}
|
||
|
||
.switch-label-text {
|
||
margin-bottom: 0 !important; /* Override label margin */
|
||
}
|
||
|
||
.switch-container {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 44px;
|
||
height: 24px;
|
||
margin-left: 12px;
|
||
}
|
||
|
||
.switch-container input {
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
.switch-slider {
|
||
position: absolute;
|
||
cursor: pointer;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(255, 255, 255, 0.14);
|
||
transition: .4s;
|
||
border-radius: 24px;
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
}
|
||
|
||
.switch-slider:before {
|
||
position: absolute;
|
||
content: "";
|
||
height: 18px;
|
||
width: 18px;
|
||
left: 2px;
|
||
bottom: 2px;
|
||
background-color: white;
|
||
transition: .4s;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
input:checked + .switch-slider {
|
||
background-color: #a0c4ff;
|
||
border-color: #4facfe;
|
||
}
|
||
|
||
input:focus + .switch-slider {
|
||
box-shadow: 0 0 1px #4facfe;
|
||
}
|
||
|
||
input:checked + .switch-slider:before {
|
||
transform: translateX(20px);
|
||
}
|
||
|
||
.form-select option {
|
||
background-color: #252525;
|
||
color: #f2f2f2;
|
||
}
|
||
|
||
.multi-select-options {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.multi-select-option {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 10px;
|
||
border-radius: 999px;
|
||
background-color: rgba(255, 255, 255, 0.04);
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
font-size: 12px;
|
||
color: rgba(242, 242, 242, 0.9);
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||
}
|
||
|
||
.multi-select-option:hover {
|
||
background-color: rgba(255, 255, 255, 0.08);
|
||
border-color: rgba(160, 196, 255, 0.6);
|
||
}
|
||
|
||
.option-label {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Variables styles */
|
||
.vars-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.input-readonly {
|
||
background-color: rgba(255, 255, 255, 0.05); /* Slightly lighter background to indicate disabled-ish but readable */
|
||
border-color: rgba(255, 255, 255, 0.05);
|
||
cursor: text; /* Allow text selection */
|
||
color: rgba(242, 242, 242, 0.6);
|
||
}
|
||
|
||
.input-readonly:focus {
|
||
border-color: rgba(255, 255, 255, 0.05);
|
||
box-shadow: none;
|
||
}
|
||
|
||
.vars-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.var-name {
|
||
flex: 0 0 auto;
|
||
font-weight: 600;
|
||
min-width: 90px;
|
||
margin-left: 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.var-separator {
|
||
font-size: 12px;
|
||
color: #858585;
|
||
margin: 0 5px;
|
||
}
|
||
|
||
.var-value {
|
||
flex: 1;
|
||
color: #d2d2d2;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.delete-var-button {
|
||
background: transparent;
|
||
border: none;
|
||
color: #999;
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: color 0.2s ease;
|
||
line-height: 1;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.delete-var-button:hover {
|
||
color: #ff6b6b;
|
||
}
|
||
|
||
/* List items styles */
|
||
.list-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.list-items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.var-item,
|
||
.list-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
max-width: 420px;
|
||
padding: 7px 12px;
|
||
background-color: rgba(90, 90, 90, 0.9);
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
border-radius: 10px;
|
||
color: #f2f2f2;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
font-size: 13px;
|
||
gap: 12px;
|
||
}
|
||
|
||
.var-item:hover,
|
||
.list-item:hover {
|
||
background-color: rgba(110, 110, 110, 0.95);
|
||
border-color: rgba(160, 196, 255, 0.7);
|
||
}
|
||
|
||
.item-value {
|
||
flex: 1;
|
||
}
|
||
|
||
.delete-item-button {
|
||
background: transparent;
|
||
border: none;
|
||
color: #999;
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: color 0.2s ease;
|
||
line-height: 1;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.delete-item-button:hover {
|
||
color: #ff6b6b;
|
||
}
|
||
|
||
.child-node-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
width: 100%;
|
||
}
|
||
|
||
.child-node-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.clear-child-button {
|
||
background: transparent;
|
||
border: none;
|
||
color: #999;
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: color 0.2s ease;
|
||
line-height: 1;
|
||
}
|
||
|
||
.clear-child-button:hover {
|
||
color: #ff6b6b;
|
||
}
|
||
|
||
.add-child-button,
|
||
.add-list-button,
|
||
.add-var-button {
|
||
position: relative;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||
background-color: rgba(255, 255, 255, 0.14);
|
||
color: rgba(242, 242, 242, 0.9);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.add-child-button:hover,
|
||
.add-list-button:hover,
|
||
.add-var-button:hover {
|
||
background-color: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.add-child-button .plus-icon,
|
||
.add-list-button .plus-icon,
|
||
.add-var-button .plus-icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
line-height: 0;
|
||
}
|
||
|
||
.add-child-button .plus-icon svg,
|
||
.add-list-button .plus-icon svg,
|
||
.add-var-button .plus-icon svg {
|
||
display: block;
|
||
}
|
||
|
||
.add-child-button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
border-color: rgba(255, 255, 255, 0.1);
|
||
background-color: rgba(74, 74, 74, 0.8);
|
||
color: rgba(200, 200, 200, 0.7);
|
||
}
|
||
|
||
.child-node-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.child-node-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 9px 14px;
|
||
background-color: rgba(90, 90, 90, 0.9);
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
border-radius: 10px;
|
||
color: #f2f2f2;
|
||
font-size: 13px;
|
||
gap: 12px;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||
}
|
||
|
||
.child-node-item:hover {
|
||
background-color: rgba(110, 110, 110, 0.95);
|
||
border-color: rgba(160, 196, 255, 0.7);
|
||
}
|
||
|
||
.child-node-name {
|
||
flex: 1;
|
||
}
|
||
|
||
.child-node-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.child-node-item-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.child-node-summary {
|
||
padding: 6px 12px;
|
||
margin-left: 0;
|
||
background-color: rgba(60, 60, 60, 0.6);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 8px;
|
||
color: rgba(200, 200, 200, 0.85);
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
word-wrap: break-word;
|
||
overflow-wrap: break-word;
|
||
white-space: pre-line;
|
||
}
|
||
|
||
.edit-child-button,
|
||
.delete-child-button {
|
||
padding: 6px 10px;
|
||
border-radius: 999px;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
transition: background-color 0.2s ease, color 0.2s ease;
|
||
}
|
||
|
||
.edit-child-button {
|
||
background-color: rgba(117, 117, 117, 0.95);
|
||
color: #f2f2f2;
|
||
}
|
||
|
||
.edit-child-button:hover {
|
||
background-color: rgba(136, 136, 136, 0.98);
|
||
}
|
||
|
||
.delete-child-button {
|
||
background-color: rgba(90, 90, 90, 0.9);
|
||
color: #f2f2f2;
|
||
}
|
||
|
||
.delete-child-button:hover {
|
||
background-color: rgba(110, 110, 110, 0.95);
|
||
}
|
||
|
||
.child-node-hint {
|
||
padding: 6px 8px;
|
||
color: rgba(189, 189, 189, 0.9);
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* Custom select styles */
|
||
|
||
.custom-select-wrapper {
|
||
position: relative;
|
||
height: 38px; /* Fixed height to prevent absolute child from stretching parent */
|
||
}
|
||
|
||
.custom-select-input {
|
||
cursor: pointer;
|
||
height: 100%;
|
||
}
|
||
|
||
.custom-select-input:focus {
|
||
cursor: text;
|
||
}
|
||
|
||
.custom-select-arrow {
|
||
position: absolute;
|
||
right: 12px;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.delete-item-button:hover {
|
||
color: #ff6b6b;
|
||
}
|
||
|
||
.child-node-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
width: 100%;
|
||
}
|
||
|
||
.child-node-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.clear-child-button {
|
||
background: transparent;
|
||
border: none;
|
||
color: #999;
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: color 0.2s ease;
|
||
line-height: 1;
|
||
}
|
||
|
||
.clear-child-button:hover {
|
||
color: #ff6b6b;
|
||
}
|
||
|
||
.add-child-button,
|
||
.add-list-button,
|
||
.add-var-button {
|
||
position: relative;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||
background-color: rgba(255, 255, 255, 0.14);
|
||
color: rgba(242, 242, 242, 0.9);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.add-child-button:hover,
|
||
.add-list-button:hover,
|
||
.add-var-button:hover {
|
||
background-color: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.add-child-button .plus-icon,
|
||
.add-list-button .plus-icon,
|
||
.add-var-button .plus-icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
line-height: 0;
|
||
}
|
||
|
||
.add-child-button .plus-icon svg,
|
||
.add-list-button .plus-icon svg,
|
||
.add-var-button .plus-icon svg {
|
||
display: block;
|
||
}
|
||
|
||
.add-child-button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
border-color: rgba(255, 255, 255, 0.1);
|
||
background-color: rgba(74, 74, 74, 0.8);
|
||
color: rgba(200, 200, 200, 0.7);
|
||
}
|
||
|
||
.child-node-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.child-node-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 9px 14px;
|
||
background-color: rgba(90, 90, 90, 0.9);
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
border-radius: 10px;
|
||
color: #f2f2f2;
|
||
font-size: 13px;
|
||
gap: 12px;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||
}
|
||
|
||
.child-node-item:hover {
|
||
background-color: rgba(110, 110, 110, 0.95);
|
||
border-color: rgba(160, 196, 255, 0.7);
|
||
}
|
||
|
||
.child-node-name {
|
||
flex: 1;
|
||
}
|
||
|
||
.child-node-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.child-node-item-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.child-node-summary {
|
||
padding: 6px 12px;
|
||
margin-left: 0;
|
||
background-color: rgba(60, 60, 60, 0.6);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 8px;
|
||
color: rgba(200, 200, 200, 0.85);
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
word-wrap: break-word;
|
||
overflow-wrap: break-word;
|
||
white-space: pre-line;
|
||
}
|
||
|
||
.edit-child-button,
|
||
.delete-child-button {
|
||
padding: 6px 10px;
|
||
border-radius: 999px;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
transition: background-color 0.2s ease, color 0.2s ease;
|
||
}
|
||
|
||
.edit-child-button {
|
||
background-color: rgba(117, 117, 117, 0.95);
|
||
color: #f2f2f2;
|
||
}
|
||
|
||
.edit-child-button:hover {
|
||
background-color: rgba(136, 136, 136, 0.98);
|
||
}
|
||
|
||
.delete-child-button {
|
||
background-color: rgba(90, 90, 90, 0.9);
|
||
color: #f2f2f2;
|
||
}
|
||
|
||
.delete-child-button:hover {
|
||
background-color: rgba(110, 110, 110, 0.95);
|
||
}
|
||
|
||
.child-node-hint {
|
||
padding: 6px 8px;
|
||
color: rgba(189, 189, 189, 0.9);
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* Custom select styles */
|
||
|
||
.custom-select-wrapper {
|
||
position: relative;
|
||
height: 38px; /* Fixed height to prevent absolute child from stretching parent */
|
||
}
|
||
|
||
.custom-select-input {
|
||
cursor: pointer;
|
||
height: 100%;
|
||
}
|
||
|
||
.custom-select-input:focus {
|
||
cursor: text;
|
||
}
|
||
|
||
.custom-select-arrow {
|
||
position: absolute;
|
||
right: 12px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: rgba(242, 242, 242, 0.7);
|
||
font-size: 12px;
|
||
pointer-events: none;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.custom-select-arrow.arrow-open {
|
||
transform: translateY(-50%) rotate(180deg);
|
||
}
|
||
|
||
:global(.custom-select-dropdown) {
|
||
position: fixed;
|
||
z-index: 10000;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
background-color: #1e1e1e;
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
border-radius: 8px;
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:global(.custom-select-option) {
|
||
padding: 10px 14px;
|
||
cursor: pointer;
|
||
color: #f2f2f2;
|
||
font-size: 13px;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
:global(.custom-select-option:hover) {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
:global(.custom-select-option.option-selected) {
|
||
background-color: rgba(160, 196, 255, 0.2);
|
||
color: #a0c4ff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
:global(.custom-select-no-results) {
|
||
padding: 10px 12px;
|
||
color: rgba(189, 189, 189, 0.7);
|
||
font-size: 12px;
|
||
text-align: center;
|
||
font-style: italic;
|
||
}
|
||
|
||
.select-disabled .custom-select-input {
|
||
cursor: not-allowed;
|
||
}
|
||
</style>
|