mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-20 10:40:29 +01:00
c3f73c0de3
* updates * updates * updates * updates * updates * moves api key CTA to top right * updates * more updates * more updates * Fix active state when "Account" is selected * Update wording in subnav: Profile becomes Account * Fix email test * Update Emails wording * Try to fix email test * Make General first tab in store settings Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
425 lines
21 KiB
Plaintext
425 lines
21 KiB
Plaintext
@model (string templateId, string title)
|
|
|
|
<div id="template-editor-app" v-cloak>
|
|
<div class="form-group">
|
|
<h3 class="mt-5 mb-4">@Model.title </h3>
|
|
@if (ViewContext.ViewData.ModelState.TryGetValue(Model.templateId, out var errors))
|
|
{
|
|
foreach (var error in errors.Errors)
|
|
{
|
|
<br/>
|
|
<span class="text-danger">@error.ErrorMessage</span>
|
|
}
|
|
}
|
|
|
|
<div class="bg-light card">
|
|
<div class="card-body " v-bind:class="{ 'card-deck': items.length > 0}">
|
|
<div v-for="(item, index) of items" class="card my-2 card-wrapper template-item me-0 ms-0" v-bind:key="item.id">
|
|
<div v-if="anyImages" class="card-img-top border-bottom" v-bind:style="getImage(item)"></div>
|
|
<div class="card-body">
|
|
<h6 class="card-title" v-html="item.title"></h6>
|
|
<button type="button" class="btn btn-primary" v-on:click="editItem(index)">Edit</button>
|
|
<button type="button" class="btn btn-danger" v-on:click="removeItem(index)">
|
|
<i class="fa fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div v-if="!items || items.length === 0" class="col-12 text-center">
|
|
No items.<br/>
|
|
<button type="button" class="btn btn-link" v-on:click="editItem(-1)" id="btn-add-first">
|
|
Add your first item
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer text-start p-3">
|
|
<button type="button" class="btn btn-primary" v-on:click="editItem(-1)" id="btn-add">
|
|
<i class="fa fa-plus fa-fw"></i> Add
|
|
</button>
|
|
<button type="button" class="btn btn-secondary ms-2" v-on:click="toggleTemplateElement()" id="ToggleRawEditor">
|
|
Toggle raw editor
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal" id="product-modal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" v-if="editingItem">{{editingItem.index>=0? "Edit" : "Create"}} item</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" ref="close">
|
|
<vc:icon symbol="close"/>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body" v-if="editingItem">
|
|
<div class="mb-3">
|
|
<span class="text-danger row m-2" v-for="error of errors">{{error}}</span>
|
|
<div class="form-group row">
|
|
<div class="col-sm-6">
|
|
<label class="form-label" data-required>Title</label>
|
|
<input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.title" autofocus ref="txtTitle"/>
|
|
</div>
|
|
<div class="col-sm-3 px-0">
|
|
<label class="form-label">Price</label>
|
|
<select class="form-select" v-model="editingItem.custom">
|
|
<option v-for="option in customPriceOptions" :value="option.value">{{option.text}}</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-sm-3" v-show="editingItem.custom !== 'topup'">
|
|
|
|
<label class="form-label"> </label>
|
|
<input class="form-control mb-2"
|
|
inputmode="numeric"
|
|
pattern="\d*"
|
|
step="any"
|
|
min="0"
|
|
type="number"
|
|
required
|
|
v-model="editingItem.price" ref="txtPrice"/>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Image</label>
|
|
<input type="text" class="form-control mb-2" pattern="[^\*#]+" v-model="editingItem.image" ref="txtImage"/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Description</label>
|
|
<textarea rows="3" cols="40" class="form-control mb-2" v-model="editingItem.description" ref="txtDescription"></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Inventory (leave blank to not use inventory feature)</label>
|
|
<input type="number" min="0" step="1" class="form-control mb-2" v-model="editingItem.inventory" ref="txtInventory"/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Id (leave blank to generate from title)</label>
|
|
<input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.id" ref="txtId"/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Buy Button Text</label>
|
|
<input type="text" id="BuyButtonText" class="form-control mb-2" v-model="editingItem.buyButtonText" ref="txtBuyButtonText"/>
|
|
</div>
|
|
<div class="form-group d-flex align-items-center">
|
|
<input type="checkbox" id="Disabled" class="btcpay-toggle me-2" v-model="editingItem.disabled"/>
|
|
<label class="form-label mb-0">Disabled</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" v-on:click="clearEditingItem()">Close</button>
|
|
<button type="button" class="btn btn-primary" v-on:click="saveEditingItem()" id="SaveItemChanges">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
new Vue({
|
|
el: '#template-editor-app',
|
|
data: {
|
|
errors: [],
|
|
items: [],
|
|
editingItem: null,
|
|
customPriceOptions: [
|
|
{ text: 'Fixed', value: "fixed" },
|
|
{ text: 'Minimum', value: "minimum" },
|
|
{ text: 'Custom', value: 'topup' },
|
|
],
|
|
elementId: "@Model.templateId"
|
|
},
|
|
computed: {
|
|
anyImages: function(){
|
|
return !!this.items.find(function(i){ return !!i.image;});
|
|
}
|
|
},
|
|
mounted: function() {
|
|
this.loadYml();
|
|
this.getInputElement().on("input change", this.loadYml.bind(this));
|
|
this.getModalElement().on("hide.bs.modal", this.clearEditingItem.bind(this));
|
|
this.toggleTemplateElement();
|
|
},
|
|
methods: {
|
|
toggleTemplateElement: function(){
|
|
this.getInputElement().parent().toggle();
|
|
},
|
|
getImage: function(item){
|
|
var image = this.unEscapeKey(item.image) || "~/img/img-placeholder.svg";
|
|
var url = image.startsWith("~") ? image.replace('~', window.location.pathname.substring(0, image.indexOf('/apps'))) : image;
|
|
return {
|
|
"background-image" : "url('" + url +"')",
|
|
"opacity": item.image? 1: 0.5
|
|
}
|
|
},
|
|
getInputElement : function(){ return $("#" + this.elementId); },
|
|
getModalElement : function(){ return $("#product-modal"); },
|
|
loadYml: function(){
|
|
var result = [];
|
|
var template = this.getInputElement().val().trim();
|
|
var lines = [];
|
|
var items = template.split("\n");
|
|
|
|
for (var i = 0; i < items.length; i++) {
|
|
if (items[i] === ""){
|
|
continue;
|
|
}
|
|
if (items[i].startsWith(" ")){
|
|
lines[lines.length-1]+=items[i] + "\n";
|
|
} else {
|
|
lines.push(items[i] + "\n");
|
|
}
|
|
}
|
|
|
|
// Split products from the template
|
|
for (var kl in lines) {
|
|
var line = lines[kl], product = line.split("\n"), goingThroughMethods = false,
|
|
id = null, price = null, title = null, description = null, image = null,
|
|
custom = null, buyButtonText = null, inventory = null, paymentMethods = [],
|
|
disabled = false;
|
|
|
|
for (var kp in product) {
|
|
var productProperty = product[kp].trim();
|
|
|
|
if (kp == 0) {
|
|
id = productProperty.replace(":", "");
|
|
}
|
|
if (productProperty.startsWith("-") && goingThroughMethods) {
|
|
paymentMethods.push(productProperty.substr(1));
|
|
} else {
|
|
goingThroughMethods = false;
|
|
}
|
|
|
|
if (productProperty.indexOf('price:') !== -1) {
|
|
price = parseFloat(productProperty.replace('price:', '').trim()).noExponents();
|
|
}
|
|
if (productProperty.indexOf('title:') !== -1) {
|
|
title = productProperty.replace('title:', '').trim();
|
|
}
|
|
if (productProperty.indexOf('description:') !== -1) {
|
|
description =productProperty.replace('description:', '').trim();
|
|
if (description.startsWith('"') && description.endsWith('"')){
|
|
description = description.substr(1, description.length-2);
|
|
}
|
|
description = description
|
|
.replaceAll("<br>", "\n")
|
|
.replaceAll("<br/>", "\n")
|
|
.replaceAll('\\"', '"')
|
|
}
|
|
if (productProperty.indexOf('image:') !== -1) {
|
|
image = productProperty.replace('image:', '').trim();
|
|
}
|
|
if (productProperty.indexOf('price_type:') !== -1) {
|
|
custom = productProperty.replace('price_type:', '').trim();
|
|
}
|
|
if (productProperty.indexOf('buyButtonText:') !== -1) {
|
|
buyButtonText = productProperty.replace('buyButtonText:', '').trim();
|
|
}
|
|
if (productProperty.indexOf('inventory:') !== -1) {
|
|
inventory = parseInt(productProperty.replace('inventory:', '').trim(),10);
|
|
}
|
|
if (productProperty.indexOf('payment_methods:') !== -1) {
|
|
goingThroughMethods = true;
|
|
}
|
|
if (productProperty.indexOf('disabled:') !== -1) {
|
|
disabled = productProperty.replace('disabled:', '').trim() === "true";
|
|
}
|
|
}
|
|
|
|
if (title != null) {
|
|
// Add product to the list
|
|
result.push({
|
|
id: id,
|
|
title: title,
|
|
price: price,
|
|
image: image || null,
|
|
description: description || '',
|
|
custom: custom || "fixed",
|
|
buyButtonText: buyButtonText,
|
|
inventory: isNaN(inventory)? null: inventory,
|
|
paymentMethods: paymentMethods,
|
|
disabled: disabled
|
|
});
|
|
}
|
|
}
|
|
this.items = result;
|
|
},
|
|
toYml: function(){
|
|
let template = '';
|
|
// Construct template from the product list
|
|
for (const key in this.items) {
|
|
const product = this.items[key],
|
|
id = product.id,
|
|
title = product.title,
|
|
price = product.custom === 'topup'? null : product.price??0,
|
|
image = product.image,
|
|
description = product.description,
|
|
custom = product.custom,
|
|
buyButtonText = product.buyButtonText,
|
|
inventory = product.inventory,
|
|
paymentMethods = product.paymentMethods,
|
|
disabled = product.disabled;
|
|
let itemTemplate = id+":\n";
|
|
itemTemplate += ( product.custom === 'topup'? '' : (' price: ' + parseFloat(price).noExponents() + '\n'));
|
|
itemTemplate+= ' title: ' + title + '\n';
|
|
|
|
if (description) {
|
|
itemTemplate += ' description: "' + description.replaceAll("\n", "<br/>").replaceAll('"', '\\"') + '"\n';
|
|
}
|
|
if (image) {
|
|
itemTemplate += ' image: ' + image + '\n';
|
|
}
|
|
if (inventory) {
|
|
itemTemplate += ' inventory: ' + inventory + '\n';
|
|
}
|
|
if (custom != null) {
|
|
itemTemplate += ' price_type: "' + custom + '" \n';
|
|
}
|
|
if (buyButtonText != null && buyButtonText.length > 0) {
|
|
itemTemplate += ' buyButtonText: ' + buyButtonText + '\n';
|
|
}
|
|
if (disabled != null) {
|
|
itemTemplate += ' disabled: ' + disabled.toString() + '\n';
|
|
}
|
|
if(paymentMethods != null && paymentMethods.length > 0){
|
|
itemTemplate+= ' payment_methods:\n';
|
|
for (var method of paymentMethods){
|
|
itemTemplate+= ' - '+method+'\n';
|
|
}
|
|
}
|
|
itemTemplate += '\n';
|
|
template+=itemTemplate;
|
|
}
|
|
this.getInputElement().val(template);
|
|
},
|
|
editItem: function(index){
|
|
this.errors = [];
|
|
if(index < 0){
|
|
this.editingItem = {index:-1, id:"", title: "", price: 0, image: "", description: "", custom: "fixed", inventory: null, paymentMethods: [], disabled: false};
|
|
}else{
|
|
this.editingItem = {...this.items[index], index};
|
|
}
|
|
|
|
this.editingItem = this.unEscape(this.editingItem);
|
|
this.getModalElement().modal("show");
|
|
},
|
|
removeItem: function(index){
|
|
this.items.splice(index,1);
|
|
this.toYml();
|
|
},
|
|
clearEditingItem: function(){
|
|
this.editingItem = null;
|
|
this.errors = [];
|
|
},
|
|
validate: function(){
|
|
this.errors = [];
|
|
if (this.editingItem.id) {
|
|
var matchedId = this.items.findIndex((x)=> { return this.unEscapeKey(x.id) === this.editingItem.id;});
|
|
if( matchedId>= 0 && matchedId != this.editingItem.index)
|
|
this.errors.push("You cannot have multiple items with the same id");
|
|
|
|
if (!this.$refs.txtId.checkValidity()) {
|
|
this.errors.push("Id is required and cannot have * or #");
|
|
}
|
|
if(this.editingItem.id.startsWith("- ")){
|
|
this.errors.push("Id cannot start with \"- \"");
|
|
}else if(this.editingItem.id.trim() == ""){
|
|
this.errors.push("Id is required");
|
|
}
|
|
}
|
|
|
|
if (this.editingItem.description.indexOf("*") >= 0 || this.editingItem.description.indexOf("#") >= 0) {
|
|
this.errors.push("Description cannot have * or #");
|
|
}
|
|
if(this.editingItem.description.startsWith("- ")){
|
|
this.errors.push("Description cannot start with \"- \"");
|
|
}
|
|
if (!this.$refs.txtImage.checkValidity()) {
|
|
this.errors.push("Image cannot have * or #");
|
|
}
|
|
if(this.editingItem.image.startsWith("- ")){
|
|
this.errors.push("Image cannot start with \"- \"");
|
|
}
|
|
|
|
if (this.editingItem.custom !== "topup" && !this.$refs.txtPrice.checkValidity()) {
|
|
this.errors.push("Price must be a valid number");
|
|
}
|
|
if (!this.$refs.txtTitle.checkValidity()) {
|
|
this.errors.push("Title is required and cannot have * or #");
|
|
}else if(this.editingItem.title.startsWith("- ")){
|
|
this.errors.push("Title cannot start with \"- \"");
|
|
}else if(this.editingItem.title.trim() == ""){
|
|
this.errors.push("Title is required");
|
|
}
|
|
if (!this.$refs.txtInventory.checkValidity()) {
|
|
this.errors.push("Inventory must be blank or a a valid number (>=0)");
|
|
}
|
|
return this.errors.length === 0;
|
|
},
|
|
saveEditingItem: function(){
|
|
const fallbackId = this.editingItem.title.toLowerCase().trim();
|
|
if(!this.editingItem.id && fallbackId){
|
|
this.editingItem.id = fallbackId;
|
|
this.$nextTick(this.saveEditingItem.bind(this));
|
|
return;
|
|
}
|
|
if(!this.validate()){
|
|
return;
|
|
}
|
|
this.editingItem = this.escape(this.editingItem);
|
|
|
|
if(this.editingItem.index < 0){
|
|
this.items.push(this.editingItem);
|
|
}else{
|
|
this.items.splice(this.editingItem.index,1,this.editingItem);
|
|
}
|
|
this.toYml();
|
|
this.getModalElement().modal("hide");
|
|
},
|
|
escape: function(item) {
|
|
for(var k in item){
|
|
if(k !== "paymentMethods" && k!=="id"){
|
|
item[k] = $('<div/>').text(item[k]).html();
|
|
}
|
|
}
|
|
return item;
|
|
},
|
|
unEscape: function(item){
|
|
for(var k in item){
|
|
if(k !== "paymentMethods" && k!=="id" && k !== "disabled"){
|
|
item[k] = this.unEscapeKey(item[k]);
|
|
}
|
|
}
|
|
return item;
|
|
},
|
|
unEscapeKey : function(k){
|
|
// Without this check a `false` boolean value will always be returned as an empty string
|
|
if (k === false) {
|
|
return "false";
|
|
}
|
|
|
|
return $('<div/>').html(k).text();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
Number.prototype.noExponents= function(){
|
|
var data= String(this).split(/[eE]/);
|
|
if(data.length== 1) return data[0];
|
|
|
|
var z= '', sign= this<0? '-':'',
|
|
str= data[0].replace('.', ''),
|
|
mag= Number(data[1])+ 1;
|
|
|
|
if(mag<0){
|
|
z= sign + '0.';
|
|
while(mag++) z += '0';
|
|
return z + str.replace(/^\-/,'');
|
|
}
|
|
mag -= str.length;
|
|
while(mag--) z += '0';
|
|
return str + z;
|
|
};
|
|
|
|
</script>
|