mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-13 19:37:37 +01:00
parent
09dbe44bca
commit
966547db54
3 changed files with 85 additions and 72 deletions
|
@ -1155,8 +1155,8 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1)")).Click();
|
||||
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
|
||||
s.Driver.FindElement(By.Id("EditorCategories-ts-control")).SendKeys("Drinks");
|
||||
s.Driver.FindElement(By.Id("ApplyItemChanges")).Click();
|
||||
|
||||
s.Driver.ScrollTo(By.Id("CodeTabButton"));
|
||||
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
|
||||
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
Assert.Contains("\"buyButtonText\": \"Take my money\"", template);
|
||||
Assert.Matches("\"categories\": \\[\n\\s+\"Drinks\"\n\\s+\\]", template);
|
||||
|
@ -1357,9 +1357,16 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.ScrollTo(By.Id("btAddItem"));
|
||||
s.Driver.FindElement(By.Id("btAddItem")).Click();
|
||||
s.Driver.FindElement(By.Id("EditorTitle")).SendKeys("Perk 1");
|
||||
s.Driver.FindElement(By.Id("EditorId")).SendKeys("Perk-1");
|
||||
s.Driver.FindElement(By.Id("EditorAmount")).SendKeys("20");
|
||||
s.Driver.FindElement(By.Id("ApplyItemChanges")).Click();
|
||||
// Test autogenerated ID
|
||||
Assert.Equal("perk-1", s.Driver.FindElement(By.Id("EditorId")).GetAttribute("value"));
|
||||
s.Driver.FindElement(By.Id("EditorId")).Clear();
|
||||
s.Driver.FindElement(By.Id("EditorId")).SendKeys("Perk-1");
|
||||
s.Driver.ScrollTo(By.Id("CodeTabButton"));
|
||||
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
|
||||
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
Assert.Contains("\"title\": \"Perk 1\"", template);
|
||||
Assert.Contains("\"id\": \"Perk-1\"", template);
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
|
|
|
@ -15,24 +15,26 @@
|
|||
<div id="item-form" class="item" v-show="!!editingItem">
|
||||
<div class="form-group">
|
||||
<label for="EditorTitle" class="form-label" data-required>Title</label>
|
||||
<input id="EditorTitle" required class="form-control mb-2" v-model="editingItem && editingItem.title" ref="inputTitle" />
|
||||
<input id="EditorTitle" required class="form-control mb-2" v-model="editingItem && editingItem.title" v-on:change="onTitleChange" />
|
||||
<div class="text-danger mb-3" v-if="errors.title">{{errors.title}}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="EditorId" class="form-label" data-required>ID</label>
|
||||
<input id="EditorId" required class="form-control mb-2" v-model="editingItem && editingItem.id" ref="inputtId" />
|
||||
<div class="form-text">Leave blank to generate ID from title.</div>
|
||||
<input id="EditorId" required class="form-control mb-2" v-model="editingItem && editingItem.id" v-on:change="onIdChange" />
|
||||
<div class="text-danger mb-3" v-if="errors.id">{{errors.id}}</div>
|
||||
<div class="form-text" v-else>Leave blank to generate ID from title.</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6">
|
||||
<label for="EditorPrice" class="form-label">Price</label>
|
||||
<select id="EditorPrice" class="form-select" v-model="editingItem && editingItem.priceType">
|
||||
<select id="EditorPrice" class="form-select" v-model="editingItem && editingItem.priceType" v-on:change="onPriceTypeChange">
|
||||
<option v-for="option in customPriceOptions" :value="option.value">{{option.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6" v-show="editingItem && editingItem.priceType !== 'Topup'">
|
||||
<label for="EditorAmount" class="form-label"> </label>
|
||||
<div class="input-group mb-2">
|
||||
<input class="form-control"
|
||||
<input class="form-control hide-number-spin"
|
||||
id="EditorAmount"
|
||||
inputmode="decimal"
|
||||
pattern="\d*"
|
||||
|
@ -41,11 +43,12 @@
|
|||
type="number"
|
||||
required
|
||||
v-model="editingItem && editingItem.price"
|
||||
ref="inputPrice"
|
||||
v-on:change="onPriceChange"
|
||||
aria-describedby="currency-addon" />
|
||||
<span class="input-group-text" id="currency-addon" v-pre>@Model.currency</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-danger" v-if="errors.price">{{errors.price}}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="EditorImageUrl" class="form-label">Image Url</label>
|
||||
|
@ -63,7 +66,7 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label for="EditorInventory" class="form-label">Inventory</label>
|
||||
<input id="EditorInventory" type="number" inputmode="numeric" min="0" step="1" class="form-control mb-2" v-model="editingItem && editingItem.inventory" ref="inputInventory" />
|
||||
<input id="EditorInventory" type="number" inputmode="numeric" min="0" step="1" class="form-control mb-2" v-model="editingItem && editingItem.inventory" v-on:change="onInventoryChange" />
|
||||
<div class="form-text">Leave blank to not use this feature.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -75,8 +78,6 @@
|
|||
<label for="Disabled" class="form-check-label">Enable</label>
|
||||
</div>
|
||||
<vc:ui-extension-point location="app-template-editor-item-detail" model="Model"></vc:ui-extension-point>
|
||||
<div class="text-danger mb-3" v-for="error of errors">{{error}}</div>
|
||||
<button class="btn btn-primary d-none d-xl-inline-block" type="button" id="ApplyItemChanges" v-on:click="apply">Apply</button>
|
||||
</div>
|
||||
<div v-if="!editingItem">Select an item to edit</div>
|
||||
</div>
|
||||
|
|
|
@ -90,7 +90,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
errors: [],
|
||||
errors: {},
|
||||
editingItem: null,
|
||||
categoriesSelect: null,
|
||||
customPriceOptions: [
|
||||
|
@ -106,74 +106,79 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
validate () {
|
||||
this.errors = [];
|
||||
|
||||
toId(value) {
|
||||
return value.toLowerCase().trim().replace(/\W+/gi, '-')
|
||||
},
|
||||
onTitleChange(e) {
|
||||
const $input = e.target;
|
||||
$input.classList.toggle('is-invalid', !$input.checkValidity())
|
||||
if (!$input.checkValidity()) {
|
||||
Vue.set(this.errors, 'title', 'Title is required')
|
||||
} else if (this.editingItem.title.startsWith('-')){
|
||||
Vue.set(this.errors, 'title', 'Title cannot start with "-"')
|
||||
} else if (!this.editingItem.title.trim()){
|
||||
Vue.set(this.errors, 'title', 'Title is required')
|
||||
} else {
|
||||
Vue.delete(this.errors, 'title')
|
||||
}
|
||||
// set id from title if not set
|
||||
if (!this.editingItem.id) {
|
||||
this.editingItem.id = this.toId(this.editingItem.title)
|
||||
Vue.delete(this.errors, 'id')
|
||||
}
|
||||
},
|
||||
onIdChange(e) {
|
||||
// set id from title if not set
|
||||
if (!this.editingItem.id) this.editingItem.id = this.toId(this.editingItem.title)
|
||||
// validate
|
||||
const $input = e.target;
|
||||
$input.classList.toggle('is-invalid', !$input.checkValidity())
|
||||
if (this.editingItem.id) {
|
||||
const existingItem = this.$parent.items.find(i => i.id === this.editingItem.id);
|
||||
if (existingItem && existingItem.id !== this.item.id)
|
||||
this.errors.push(`An item with the ID "${this.editingItem.id}" already exists`);
|
||||
Vue.set(this.errors, 'id', `An item with the ID "${this.editingItem.id}" already exists`)
|
||||
if (this.editingItem.id.startsWith('-'))
|
||||
this.errors.push('ID cannot start with "-"');
|
||||
Vue.set(this.errors, 'id', 'ID cannot start with "-"')
|
||||
else if (this.editingItem.id.trim() === '')
|
||||
this.errors.push('ID is required');
|
||||
Vue.set(this.errors, 'id', 'ID is required')
|
||||
else
|
||||
Vue.delete(this.errors, 'id')
|
||||
} else {
|
||||
this.errors.push('ID is required');
|
||||
Vue.set(this.errors, 'id', 'ID is required')
|
||||
}
|
||||
|
||||
const { inputTitle, inputPrice, inputInventory } = this.$refs
|
||||
Object.keys(this.$refs).forEach(ref => {
|
||||
if (ref.startsWith('input')) {
|
||||
const $ref = this.$refs[ref];
|
||||
$ref.classList.toggle('is-invalid', !$ref.checkValidity())
|
||||
}
|
||||
})
|
||||
|
||||
if (this.editingItem.priceType !== 'Topup' && !inputPrice.checkValidity()) {
|
||||
this.errors.push('Price must be a valid number');
|
||||
}
|
||||
|
||||
if (!inputTitle.checkValidity()) {
|
||||
this.errors.push('Title is required');
|
||||
} 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 (!inputInventory.checkValidity()) {
|
||||
this.errors.push('Inventory must not be set or be a valid number (>=0)');
|
||||
}
|
||||
|
||||
return this.errors.length === 0;
|
||||
},
|
||||
apply() {
|
||||
// set id from title if not set
|
||||
if (!this.editingItem.id) this.editingItem.id = this.editingItem.title.toLowerCase().trim();
|
||||
// validate
|
||||
if (!this.validate()) return;
|
||||
// set item props
|
||||
Object.keys(this.editingItem).forEach(prop => {
|
||||
const value = this.editingItem[prop];
|
||||
Vue.set(this.$parent.selectedItem, prop, value);
|
||||
})
|
||||
// remove empty/non-existing props on item
|
||||
Object.keys(this.item).forEach(prop => {
|
||||
const value = this.editingItem[prop];
|
||||
if (typeof value === 'undefined' || value === null) {
|
||||
Vue.delete(this.$parent.selectedItem, prop);
|
||||
onInventoryChange(e) {
|
||||
const $input = e.target;
|
||||
$input.classList.toggle('is-invalid', !$input.checkValidity())
|
||||
if (!$input.checkValidity()) {
|
||||
Vue.set(this.errors, 'inventory', 'Inventory must not be set or be a valid number (>=0)')
|
||||
}
|
||||
},
|
||||
onPriceChange(e) {
|
||||
const $input = e.target;
|
||||
$input.classList.toggle('is-invalid', !$input.checkValidity())
|
||||
if (this.editingItem.priceType !== 'Topup' && !$input.checkValidity()) {
|
||||
Vue.set(this.errors, 'price', 'Price must be a valid number')
|
||||
} else {
|
||||
Vue.delete(this.errors, 'price')
|
||||
}
|
||||
},
|
||||
onPriceTypeChange(e) {
|
||||
const $input = e.target;
|
||||
$input.classList.toggle('is-invalid', !$input.checkValidity())
|
||||
if ($input.value === 'Topup') {
|
||||
Vue.set(this.editingItem, 'price', null)
|
||||
}
|
||||
})
|
||||
// update categories
|
||||
this.categoriesSelect.clearOptions();
|
||||
this.categoriesSelect.addOptions(this.allCategories.map(value => ({ value, text: value })));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
item: function (newItem) {
|
||||
this.errors = [];
|
||||
this.editingItem = newItem ? { ...newItem } : null;
|
||||
item(newItem) {
|
||||
this.errors = {};
|
||||
this.editingItem = newItem;
|
||||
if (this.editingItem != null) {
|
||||
// update categories
|
||||
this.categoriesSelect.clearOptions();
|
||||
this.categoriesSelect.addOptions(this.allCategories.map(value => ({ value, text: value })));
|
||||
this.categoriesSelect.setValue(this.editingItem.categories);
|
||||
}
|
||||
}
|
||||
|
@ -187,11 +192,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
this.categoriesSelect.on('change', () => {
|
||||
const value = this.categoriesSelect.getValue();
|
||||
this.editingItem.categories = Array.from(value.split(',').reduce((res, item) => {
|
||||
Vue.set(this.editingItem, 'categories', Array.from(value.split(',').reduce((res, item) => {
|
||||
const category = item.trim();
|
||||
if (category) res.add(category);
|
||||
return res;
|
||||
}, new Set()));
|
||||
}, new Set())))
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
|
|
Loading…
Add table
Reference in a new issue