mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Form invoice amount adjusters (#5158)
* Fix constant fields being editable on UI * fix redirect to checkout if invoice is settled (redirect to receipt instead) * enhance: make mirror field type able to map values * Introduce invoice amount adjustment fields for form * Integrate invoice amount adjustment fields for form on pos * Support mirror in editor * Indicate when special field names are used * polsih mirror view and name suggestions for fields * clarify * hide hidden field from ui * Minor adjustmentts * Improve mirror field editing --------- Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
parent
22435a2bf5
commit
19d5e64063
7 changed files with 231 additions and 88 deletions
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
|
@ -10,7 +11,7 @@ public class FieldValueMirror : IFormComponentProvider
|
|||
{
|
||||
if (form.GetFieldByFullName(field.Value) is null)
|
||||
{
|
||||
field.ValidationErrors = new List<string> { $"{field.Name} requires {field.Value} to be present" };
|
||||
field.ValidationErrors = new List<string> {$"{field.Name} requires {field.Value} to be present"};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +22,13 @@ public class FieldValueMirror : IFormComponentProvider
|
|||
|
||||
public string GetValue(Form form, Field field)
|
||||
{
|
||||
return form.GetFieldByFullName(field.Value)?.Value;
|
||||
var rawValue = form.GetFieldByFullName(field.Value)?.Value;
|
||||
if (rawValue is not null && field.AdditionalData?.TryGetValue("valuemap", out var valueMap) is true &&
|
||||
valueMap is JObject map && map.TryGetValue(rawValue, out var mappedValue))
|
||||
{
|
||||
return mappedValue.Value<string>();
|
||||
}
|
||||
|
||||
return rawValue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,11 +151,32 @@ public class FormDataService
|
|||
|
||||
public CreateInvoiceRequest GenerateInvoiceParametersFromForm(Form form)
|
||||
{
|
||||
var amt = GetValue(form, $"{InvoiceParameterPrefix}amount");
|
||||
var amtRaw = GetValue(form, $"{InvoiceParameterPrefix}amount");
|
||||
var amt = string.IsNullOrEmpty(amtRaw) ? (decimal?) null : decimal.Parse(amtRaw, CultureInfo.InvariantCulture);
|
||||
var adjustmentAmount = 0m;
|
||||
foreach (var adjustmentField in form.GetAllFields().Where(f => f.FullName.StartsWith($"{InvoiceParameterPrefix}amount_adjustment")))
|
||||
{
|
||||
if (!decimal.TryParse(GetValue(form, adjustmentField.Field), out var adjustment))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
adjustmentAmount += adjustment;
|
||||
}
|
||||
|
||||
if (amt is null && adjustmentAmount > 0)
|
||||
{
|
||||
amt = adjustmentAmount;
|
||||
}
|
||||
else if(amt is not null)
|
||||
{
|
||||
amt += adjustmentAmount;
|
||||
amt = Math.Max(0, amt!.Value);
|
||||
}
|
||||
return new CreateInvoiceRequest
|
||||
{
|
||||
Currency = GetValue(form, $"{InvoiceParameterPrefix}currency"),
|
||||
Amount = string.IsNullOrEmpty(amt) ? null : decimal.Parse(amt, CultureInfo.InvariantCulture),
|
||||
Amount = amt,
|
||||
Metadata = GetValues(form),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -205,7 +205,10 @@ public class UIFormsController : Controller
|
|||
|
||||
var request = _formDataService.GenerateInvoiceParametersFromForm(form);
|
||||
var inv = await invoiceController.CreateInvoiceCoreRaw(request, store, Request.GetAbsoluteRoot());
|
||||
|
||||
if (inv.Price == 0 && inv.Type == InvoiceType.Standard && inv.ReceiptOptions?.Enabled is not false)
|
||||
{
|
||||
return RedirectToAction("InvoiceReceipt", "UIInvoice", new { invoiceId = inv.Id });
|
||||
}
|
||||
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = inv.Id });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -278,7 +278,28 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
|||
return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
|
||||
}
|
||||
|
||||
var amtField = form.GetFieldByFullName($"{FormDataService.InvoiceParameterPrefix}amount");
|
||||
if (amtField is null && price.HasValue)
|
||||
{
|
||||
form.Fields.Add(new Field
|
||||
{
|
||||
Name = $"{FormDataService.InvoiceParameterPrefix}amount",
|
||||
Type = "hidden",
|
||||
Value = price.ToString(),
|
||||
Constant = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
amtField.Value = price?.ToString();
|
||||
}
|
||||
formResponseJObject = FormDataService.GetValues(form);
|
||||
|
||||
var invoiceRequest = FormDataService.GenerateInvoiceParametersFromForm(form);
|
||||
if (invoiceRequest.Amount is not null)
|
||||
{
|
||||
price = invoiceRequest.Amount.Value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
try
|
||||
|
|
|
@ -3,12 +3,17 @@
|
|||
var isInvalid = ViewContext.ModelState[Model.Name]?.ValidationState is Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid;
|
||||
var errors = isInvalid ? ViewContext.ModelState[Model.Name].Errors : null;
|
||||
}
|
||||
@if (Model.Type == "hidden")
|
||||
{
|
||||
<input id="@Model.Name" type="@Model.Type" name="@Model.Name" value="@Model.Value" />
|
||||
return;
|
||||
}
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="@Model.Name"@(Model.Required ? " data-required" : "")>
|
||||
@Safe.Raw(Model.Label)
|
||||
</label>
|
||||
<input id="@Model.Name" type="@Model.Type" class="form-control @(errors is null ? "" : "is-invalid")"
|
||||
name="@Model.Name" value="@Model.Value" data-val="true"
|
||||
name="@Model.Name" value="@Model.Value" data-val="true" readonly="@Model.Constant"
|
||||
@if (!string.IsNullOrEmpty(Model.HelpText))
|
||||
{
|
||||
@Safe.Raw($" aria-describedby=\"HelpText-{Model.Name}\"")
|
||||
|
|
|
@ -30,6 +30,11 @@
|
|||
}
|
||||
|
||||
@section PageFootContent {
|
||||
<datalist id="special-field-names">
|
||||
<option value="invoice_amount">Determine the generated invoice amount</option>
|
||||
<option value="invoice_currency">Determine the generated invoice currency</option>
|
||||
<option value="invoice_amount_adjustment">Adjusts the generated invoice amount — use as a prefix to have multiple adjustment fields</option>
|
||||
</datalist>
|
||||
<template id="form-template-email">
|
||||
@FormDataService.StaticFormEmail
|
||||
</template>
|
||||
|
@ -50,8 +55,11 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-editor-field-name" class="form-label" data-required>Name</label>
|
||||
<input id="field-editor-field-name" class="form-control" required v-model="field.name" />
|
||||
<div class="form-text">The name of the field in the invoice's metadata</div>
|
||||
<input id="field-editor-field-name" class="form-control" list="special-field-names" required v-model="field.name" />
|
||||
<div class="form-text">The name of the field in the invoice's metadata.</div>
|
||||
<div class="form-text text-info" v-if="field.name === 'invoice_currency'">The configured name means the value of this field will determine the invoice currency for public forms.</div>
|
||||
<div class="form-text text-info" v-if="field.name === 'invoice_amount'">The configured name means the value of this field will determine the invoice amount for public forms.</div>
|
||||
<div class="form-text text-info" v-if="field.name && field.name.startsWith('invoice_amount_adjustment')">The configured name means the value of this field will adjust the invoice amount for public forms and the point of sale app.</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="field.type === 'select'">
|
||||
<h5 class="mt-2">Options</h5>
|
||||
|
@ -62,11 +70,11 @@
|
|||
</button>
|
||||
<div class="field flex-grow-1">
|
||||
<label :for="`field-option-value-${index}`" class="form-label">Value</label>
|
||||
<input :for="`field-option-value-${index}`" class="form-control" v-model.lazy="option.value" />
|
||||
<input :id="`field-option-value-${index}`" class="form-control" v-model.lazy="option.value" />
|
||||
</div>
|
||||
<div class="field flex-grow-1">
|
||||
<label :for="`field-option-text-${index}`" class="form-label">Text</label>
|
||||
<input :for="`field-option-text-${index}`" class="form-control" v-model="option.text" />
|
||||
<input :id="`field-option-text-${index}`" class="form-control" v-model="option.text" />
|
||||
</div>
|
||||
<button type="button" class="btn b-0 control remove" v-on:click="removeOption($event, index)">
|
||||
<vc:icon symbol="trash" />
|
||||
|
@ -78,20 +86,53 @@
|
|||
Add Option
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group" v-if="field.type !== 'fieldset'">
|
||||
<div class="form-group" v-if="field.type !== 'fieldset' && field.type !== 'mirror'">
|
||||
<label for="field-editor-field-value" class="form-label">Default Value</label>
|
||||
<input id="field-editor-field-value" class="form-control" v-model="field.value" />
|
||||
</div>
|
||||
<div class="form-group" v-if="field.type !== 'fieldset'">
|
||||
<div class="form-group" v-if="field.type === 'mirror'">
|
||||
<label for="field-editor-field-mirror" class="form-label">Field to mirror</label>
|
||||
<select id="field-editor-field-mirror" class="form-select" v-model="field.value">
|
||||
<option v-for="option in $root.allFields" v-if="option.name && option.name !== field.name" :key="option.name" :value="option.name" :selected="option.name === field.value" v-text="option.label || option.name"></option>
|
||||
</select>
|
||||
<div class="form-text">The chosen field's selected value will be copied to this field upon submission.</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="field.type === 'mirror'">
|
||||
<h5 class="mt-2">Value Mapper</h5>
|
||||
<div class="form-text">The values being mirrored from another field will be mapped to another value if configured.</div>
|
||||
<div class="options">
|
||||
<div v-if="field.valuemap" v-for="(v, k, index) in field.valuemap" :key="k" class="d-flex align-items-start gap-2 pt-3">
|
||||
<div class="field flex-grow-1">
|
||||
<label :for="`field-valuemap-value-${index}`" class="form-label">Original Value</label>
|
||||
<select v-if="mirroredField && mirroredField.type === 'select'" :id="`field-valuemap-value-${index}`" class="form-select" v-on:change="updateValueMap(k, $event.target.value, v)">
|
||||
<option v-for="option in mirroredField.options" v-if="option.text && option.value" :key="option.value" :value="option.value" :selected="k === option.value" v-text="`${option.value} (${option.text})`"></option>
|
||||
</select>
|
||||
<input v-else :id="`field-valuemap-value-${index}`" class="form-control" placeholder="Value to match" :value="k" v-on:change="updateValueMap(k, $event.target.value, v)" />
|
||||
</div>
|
||||
<div class="field flex-grow-1">
|
||||
<label :for="`field-valuemap-mapped-${index}`" class="form-label">Mapped Value</label>
|
||||
<input :id="`field-valuemap-mapped-${index}`" class="form-control" placeholder="Value to set" :value="v" v-on:change="updateValueMap(k, k, $event.target.value)" />
|
||||
</div>
|
||||
<button type="button" class="btn b-0 control remove" v-on:click="removeValueMap($event, k)">
|
||||
<vc:icon symbol="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link px-1 py-2 gap-1 add fw-semibold d-inline-flex align-items-center" v-on:click.stop="addValueMap($event)">
|
||||
<vc:icon symbol="new" />
|
||||
Add mapped value
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group" v-if="field.type !== 'fieldset' && field.type !== 'mirror'">
|
||||
<label for="field-editor-field-helpText" class="form-label">Helper Text</label>
|
||||
<input id="field-editor-field-helpText" class="form-control" v-model="field.helpText" />
|
||||
<div class="form-text">Additional text to provide an explanation for the field</div>
|
||||
</div>
|
||||
<div class="form-group form-check" v-if="field.type !== 'fieldset'">
|
||||
<div class="form-group form-check" v-if="field.type !== 'fieldset' && field.type !== 'mirror'">
|
||||
<input id="field-editor-field-required" type="checkbox" class="form-check-input" v-model="field.required" />
|
||||
<label for="field-editor-field-required" class="form-check-label">Required Field</label>
|
||||
</div>
|
||||
<div class="form-group form-check" v-if="field.type !== 'fieldset' && field.type !== 'select'">
|
||||
<div class="form-group form-check" v-if="field.type !== 'fieldset' && field.type !== 'select' && field.type !== 'mirror'">
|
||||
<input id="field-editor-field-constant" type="checkbox" class="form-check-input" v-model="field.constant" />
|
||||
<label for="field-editor-field-constant" class="form-check-label">Constant</label>
|
||||
<div class="form-text">The user will not be able to change the field's value</div>
|
||||
|
@ -143,6 +184,12 @@
|
|||
<div v-if="helpText" :id="`HelpText-${name}`" class="form-text" v-sanitize="helpText"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="field-type-mirror">
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label" v-text="label" v-if="label"></label>
|
||||
<div class="form-text">Mirror of {{value}}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="field-type-fieldset">
|
||||
<fieldset>
|
||||
<legend class="h5 mt-1 mb-2" v-text="label"></legend>
|
||||
|
@ -157,84 +204,84 @@
|
|||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
|
||||
<form method="post" asp-action="Modify" asp-route-id="@formId" asp-route-storeId="@storeId">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">
|
||||
<span>@ViewData["Title"]</span>
|
||||
<a href="https://docs.btcpayserver.org/Forms" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveButton">Save</button>
|
||||
@if (!isNew)
|
||||
{
|
||||
<a class="btn btn-secondary" asp-action="ViewPublicForm" asp-route-formId="@formId" id="ViewForm">View</a>
|
||||
}
|
||||
<form method="post" asp-action="Modify" asp-route-id="@formId" asp-route-storeId="@storeId">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">
|
||||
<span>@ViewData["Title"]</span>
|
||||
<a href="https://docs.btcpayserver.org/Forms" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveButton">Save</button>
|
||||
@if (!isNew)
|
||||
{
|
||||
<a class="btn btn-secondary" asp-action="ViewPublicForm" asp-route-formId="@formId" id="ViewForm">View</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group" style="max-width: 27rem;">
|
||||
<label asp-for="Name" class="form-label" data-required></label>
|
||||
<input asp-for="Name" class="form-control" required />
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-4 gap-3">
|
||||
<input asp-for="Public" type="checkbox" class="btcpay-toggle" />
|
||||
<div>
|
||||
<label asp-for="Public"></label>
|
||||
<div class="form-text">
|
||||
Standalone mode, which can be used to generate invoices
|
||||
independent of payment requests or apps.
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group" style="max-width: 27rem;">
|
||||
<label asp-for="Name" class="form-label" data-required></label>
|
||||
<input asp-for="Name" class="form-control" required />
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-4 gap-3">
|
||||
<input asp-for="Public" type="checkbox" class="btcpay-toggle" />
|
||||
<div>
|
||||
<label asp-for="Public"></label>
|
||||
<div class="form-text">
|
||||
Standalone mode, which can be used to generate invoices
|
||||
independent of payment requests or apps.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="FormEditor">
|
||||
<div class="d-flex flex-wrap align-items-end justify-content-between gap-3 mb-3">
|
||||
<ul class="nav nav-pills gap-4" id="form-editor-tab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="EditorTabButton" data-bs-toggle="pill" data-bs-target="#EditorTabPane" type="button" role="tab" aria-controls="EditorTabPane" aria-selected="true">Editor</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="CodeTabButton" data-bs-toggle="pill" data-bs-target="#CodeTabPane" type="button" role="tab" aria-controls="CodeTabPane" aria-selected="false">Code</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<span class="fw-semibold">Templates</span>
|
||||
<button type="button" class="btn btn-link p-0 fw-semibold" v-on:click="applyTemplate('email')" id="ApplyEmailTemplate">Email</button>
|
||||
<button type="button" class="btn btn-link p-0 fw-semibold" v-on:click="applyTemplate('address')" id="ApplyAddressTemplate">Address</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="EditorTabPane" role="tabpanel" aria-labelledby="EditorTabButton" tabindex="0">
|
||||
<div class="row align-items-start">
|
||||
<div class="col-lg-7 mb-4 mb-lg-0">
|
||||
<fields-editor :path="[]"
|
||||
:fields="fields"
|
||||
:selected-field="selectedField"
|
||||
v-on:add-field="addField"
|
||||
v-on:sort-fields="sortFields"
|
||||
v-on:select-field="selectField"
|
||||
v-on:remove-field="removeField"
|
||||
class="bg-tile pb-2 rounded" />
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<field-editor :field="selectedField" class="bg-tile p-4 rounded" />
|
||||
</div>
|
||||
|
||||
<div id="FormEditor">
|
||||
<div class="d-flex flex-wrap align-items-end justify-content-between gap-3 mb-3">
|
||||
<ul class="nav nav-pills gap-4" id="form-editor-tab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="EditorTabButton" data-bs-toggle="pill" data-bs-target="#EditorTabPane" type="button" role="tab" aria-controls="EditorTabPane" aria-selected="true">Editor</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="CodeTabButton" data-bs-toggle="pill" data-bs-target="#CodeTabPane" type="button" role="tab" aria-controls="CodeTabPane" aria-selected="false">Code</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<span class="fw-semibold">Templates</span>
|
||||
<button type="button" class="btn btn-link p-0 fw-semibold" v-on:click="applyTemplate('email')" id="ApplyEmailTemplate">Email</button>
|
||||
<button type="button" class="btn btn-link p-0 fw-semibold" v-on:click="applyTemplate('address')" id="ApplyAddressTemplate">Address</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="CodeTabPane" role="tabpanel" aria-labelledby="CodeTabButton" tabindex="0">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<label asp-for="FormConfig" class="form-label" data-required>Form JSON</label>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="EditorTabPane" role="tabpanel" aria-labelledby="EditorTabButton" tabindex="0">
|
||||
<div class="row align-items-start">
|
||||
<div class="col-lg-7 mb-4 mb-lg-0">
|
||||
<fields-editor :path="[]"
|
||||
:fields="fields"
|
||||
:selected-field="selectedField"
|
||||
v-on:add-field="addField"
|
||||
v-on:sort-fields="sortFields"
|
||||
v-on:select-field="selectField"
|
||||
v-on:remove-field="removeField"
|
||||
class="bg-tile pb-2 rounded" />
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<field-editor :field="selectedField" class="bg-tile p-4 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="CodeTabPane" role="tabpanel" aria-labelledby="CodeTabButton" tabindex="0">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<label asp-for="FormConfig" class="form-label" data-required>Form JSON</label>
|
||||
</div>
|
||||
<textarea asp-for="FormConfig" class="form-control font-monospace" style="font-size:.85rem" rows="21" cols="21" v-model="configJSON" v-on:change="updateFromJSON"></textarea>
|
||||
<span asp-validation-for="FormConfig" class="text-danger"></span>
|
||||
</div>
|
||||
<textarea asp-for="FormConfig" class="form-control font-monospace" style="font-size:.85rem" rows="21" cols="21" v-model="configJSON" v-on:change="updateFromJSON"></textarea>
|
||||
<span asp-validation-for="FormConfig" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
|
|
|
@ -9,7 +9,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const $config = document.getElementById('FormConfig')
|
||||
let config = parseConfig($config.value) || {}
|
||||
|
||||
const specialFieldTypeOptions = ['fieldset', 'textarea', 'select']
|
||||
const specialFieldTypeOptions = ['fieldset', 'textarea', 'select', 'mirror']
|
||||
const inputFieldTypeOptions = ['text', 'number', 'password', 'email', 'url', 'tel', 'date', 'hidden']
|
||||
const fieldTypeOptions = inputFieldTypeOptions.concat(specialFieldTypeOptions)
|
||||
|
||||
|
@ -57,11 +57,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
options: Array
|
||||
}
|
||||
})
|
||||
const FieldTypeMirror = Vue.extend({
|
||||
mixins: [fieldTypeBase],
|
||||
name: 'field-type-mirror',
|
||||
template: '#field-type-mirror'
|
||||
})
|
||||
|
||||
const components = {
|
||||
FieldTypeInput,
|
||||
FieldTypeSelect,
|
||||
FieldTypeTextarea
|
||||
FieldTypeTextarea,
|
||||
FieldTypeMirror
|
||||
}
|
||||
|
||||
// register fields-editor and field-type-fieldset globally in order to use them recursively
|
||||
|
@ -100,6 +106,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
path: Array,
|
||||
field: fieldProps
|
||||
},
|
||||
computed: {
|
||||
mirroredField() {
|
||||
return this.field.type === 'mirror' &&
|
||||
this.$root.allFields.find(f => f.name === this.field.value)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFieldComponent,
|
||||
addOption (event) {
|
||||
|
@ -114,7 +126,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
sortOptions (event) {
|
||||
const { newIndex, oldIndex } = event
|
||||
this.field.options.splice(newIndex, 0, this.field.options.splice(oldIndex, 1)[0])
|
||||
}
|
||||
},
|
||||
addValueMap (event) {
|
||||
if (!this.field.valuemap) this.$set(this.field, 'valuemap', {})
|
||||
const index = Object.keys(this.field.valuemap).length + 1;
|
||||
this.$set(this.field.valuemap, `valuemap_${index}`, '')
|
||||
},
|
||||
updateValueMap(oldK, newK, newV) {
|
||||
if (oldK !== newK) {
|
||||
Vue.delete(this.field.valuemap, oldK);
|
||||
}
|
||||
Vue.set(this.field.valuemap, newK, newV);
|
||||
},
|
||||
removeValueMap(event, k) {
|
||||
Vue.delete(this.field.valuemap, k);
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -131,6 +157,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
allFields() {
|
||||
const getFields = (fields, path) => {
|
||||
let result = [];
|
||||
for (const field of fields) {
|
||||
result.push(field)
|
||||
if (field.fields && field.fields.length > 0)
|
||||
result= result.concat(getFields(field.fields, path + field.name));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return getFields(this.fields, "")
|
||||
},
|
||||
fields() {
|
||||
return this.config.fields || []
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue