Use HTMx to delete and restore invoice products
It is better that way because it works without JavaScript; if HTMx is not available, it will just use regulars forms. The problem is that most of the submit buttons where using formaction to send the request to a different action, and only one button was the “real” action. Since i could not pass the formaction to invoice-product-form template, i have changed the “default” action to the one with “ancillary” functions. I have to use a different action to remove for each product because i can not pass the index to the backend without JavaScript: it only depends on the button click, that already has a name for the action. Thus, in a way, i have “merged” the action and the index in a single name.
This commit is contained in:
parent
07a28639f2
commit
8529da1615
|
@ -20,6 +20,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const removedProductSuffix = ".removed"
|
||||||
|
|
||||||
type InvoiceEntry struct {
|
type InvoiceEntry struct {
|
||||||
Slug string
|
Slug string
|
||||||
Date time.Time
|
Date time.Time
|
||||||
|
@ -563,6 +565,7 @@ type invoiceForm struct {
|
||||||
PaymentMethod *SelectField
|
PaymentMethod *SelectField
|
||||||
Tags *TagsField
|
Tags *TagsField
|
||||||
Products []*invoiceProductForm
|
Products []*invoiceProductForm
|
||||||
|
RemovedProduct *invoiceProductForm
|
||||||
}
|
}
|
||||||
|
|
||||||
func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *invoiceForm {
|
func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *invoiceForm {
|
||||||
|
@ -665,6 +668,25 @@ func (form *invoiceForm) Update() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (form *invoiceForm) RemoveProduct(index int) {
|
||||||
|
products := form.Products
|
||||||
|
form.Products = nil
|
||||||
|
for n, product := range products {
|
||||||
|
if n == index {
|
||||||
|
form.RemovedProduct = product
|
||||||
|
} else {
|
||||||
|
if n != len(form.Products) {
|
||||||
|
product.Index = len(form.Products)
|
||||||
|
product.Rename()
|
||||||
|
}
|
||||||
|
form.Products = append(form.Products, product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if form.RemovedProduct != nil {
|
||||||
|
form.RemovedProduct.RenameWithSuffix(removedProductSuffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selectProductBySlug = `
|
const selectProductBySlug = `
|
||||||
select ''
|
select ''
|
||||||
, product_id
|
, product_id
|
||||||
|
@ -708,6 +730,23 @@ func (form *invoiceForm) mustAddProductsFromQuery(ctx context.Context, conn *Con
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (form *invoiceForm) InsertProduct(product *invoiceProductForm) {
|
||||||
|
replaced := false
|
||||||
|
for n, existing := range form.Products {
|
||||||
|
if existing.Quantity.Val == "" || existing.Quantity.Val == "0" {
|
||||||
|
product.Index = n
|
||||||
|
form.Products[n] = product
|
||||||
|
replaced = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !replaced {
|
||||||
|
product.Index = len(form.Products)
|
||||||
|
form.Products = append(form.Products, product)
|
||||||
|
}
|
||||||
|
product.Rename()
|
||||||
|
}
|
||||||
|
|
||||||
func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
|
func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
|
||||||
var invoiceId int
|
var invoiceId int
|
||||||
selectedInvoiceStatus := form.InvoiceStatus.Selected
|
selectedInvoiceStatus := form.InvoiceStatus.Selected
|
||||||
|
@ -834,7 +873,9 @@ func newInvoiceProductForm(index int, company *Company, locale *Locale, taxOptio
|
||||||
}
|
}
|
||||||
|
|
||||||
func (form *invoiceProductForm) Rename() {
|
func (form *invoiceProductForm) Rename() {
|
||||||
suffix := "." + strconv.Itoa(form.Index)
|
form.RenameWithSuffix("." + strconv.Itoa(form.Index))
|
||||||
|
}
|
||||||
|
func (form *invoiceProductForm) RenameWithSuffix(suffix string) {
|
||||||
form.InvoiceProductId.Name = "product.invoice_product_id" + suffix
|
form.InvoiceProductId.Name = "product.invoice_product_id" + suffix
|
||||||
form.ProductId.Name = "product.id" + suffix
|
form.ProductId.Name = "product.id" + suffix
|
||||||
form.Name.Name = "product.name" + suffix
|
form.Name.Name = "product.name" + suffix
|
||||||
|
@ -1018,7 +1059,8 @@ func handleInvoiceAction(w http.ResponseWriter, r *http.Request, action string,
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch r.Form.Get("action") {
|
actionField := r.Form.Get("action")
|
||||||
|
switch actionField {
|
||||||
case "update":
|
case "update":
|
||||||
form.Update()
|
form.Update()
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
@ -1030,8 +1072,31 @@ func handleInvoiceAction(w http.ResponseWriter, r *http.Request, action string,
|
||||||
form.AddProducts(r.Context(), conn, r.Form["slug"])
|
form.AddProducts(r.Context(), conn, r.Form["slug"])
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
renderForm(w, r, form)
|
renderForm(w, r, form)
|
||||||
|
case "restore-product":
|
||||||
|
restoredProduct := newInvoiceProductForm(0, company, locale, mustGetTaxOptions(r.Context(), conn, company))
|
||||||
|
restoredProduct.RenameWithSuffix(removedProductSuffix)
|
||||||
|
if err := restoredProduct.Parse(r); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
form.InsertProduct(restoredProduct)
|
||||||
|
form.Update()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
renderForm(w, r, form)
|
||||||
default:
|
default:
|
||||||
|
prefix := "remove-product."
|
||||||
|
if strings.HasPrefix(actionField, prefix) {
|
||||||
|
index, err := strconv.Atoi(actionField[len(prefix):])
|
||||||
|
if err != nil {
|
||||||
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
|
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
|
||||||
|
} else {
|
||||||
|
form.RemoveProduct(index)
|
||||||
|
form.Update()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
renderForm(w, r, form)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http.Error(w, gettext("Invalid action", locale), http.StatusBadRequest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,13 +145,13 @@
|
||||||
{{ define "invoice-product-form" -}}
|
{{ define "invoice-product-form" -}}
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}}
|
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}}
|
||||||
<fieldset class="new-invoice-product"
|
<fieldset class="new-invoice-product"
|
||||||
x-data="{removeProduct() { $el.remove(); document.getElementById('recompute-button').click(); }}"
|
|
||||||
data-hx-select="unset"
|
data-hx-select="unset"
|
||||||
data-hx-vals='{"index": {{ .Index }}}'
|
data-hx-vals='{"index": {{ .Index }}}'
|
||||||
data-hx-include="[name='product.quantity.{{ .Index }}']"
|
data-hx-include="[name='product.quantity.{{ .Index }}']"
|
||||||
>
|
>
|
||||||
<button x-cloak type="button" class="icon delete-product"
|
<button type="submit" class="icon delete-product"
|
||||||
@click="removeProduct()"
|
formnovalidate
|
||||||
|
name="action" value="remove-product.{{ .Index }}"
|
||||||
aria-label="{{( gettext "Delete product from invoice" )}}"
|
aria-label="{{( gettext "Delete product from invoice" )}}"
|
||||||
><i class="ri-delete-back-2-line"></i></button>
|
><i class="ri-delete-back-2-line"></i></button>
|
||||||
{{ template "hidden-field" .InvoiceProductId }}
|
{{ template "hidden-field" .InvoiceProductId }}
|
||||||
|
|
|
@ -17,10 +17,30 @@
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editInvoicePage*/ -}}
|
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editInvoicePage*/ -}}
|
||||||
<section id="invoice-dialog-content" data-hx-target="main">
|
<section id="invoice-dialog-content" data-hx-target="main">
|
||||||
<h2>{{ printf (pgettext "Edit Invoice “%s”" "title") .Number }}</h2>
|
<h2>{{ printf (pgettext "Edit Invoice “%s”" "title") .Number }}</h2>
|
||||||
<form method="POST" action="{{ companyURI "/invoices/" }}{{ .Slug }}" data-hx-boost="true">
|
<form method="POST" action="{{ companyURI "/invoices/" }}{{ .Slug }}/edit" data-hx-boost="true">
|
||||||
{{ csrfToken }}
|
{{ csrfToken }}
|
||||||
|
|
||||||
{{ with .Form -}}
|
{{ with .Form -}}
|
||||||
|
{{ if .RemovedProduct -}}
|
||||||
|
<div role="alert">
|
||||||
|
{{ with .RemovedProduct -}}
|
||||||
|
<p>{{printf (gettext "Product “%s” removed") .Name}}</p>
|
||||||
|
<button type="submit"
|
||||||
|
formnovalidate
|
||||||
|
name="action" value="restore-product"
|
||||||
|
>{{( pgettext "Undo" "action" )}}</button>
|
||||||
|
{{ template "hidden-field" .InvoiceProductId }}
|
||||||
|
{{ template "hidden-field" .ProductId }}
|
||||||
|
{{ template "hidden-field" .Name }}
|
||||||
|
{{ template "hidden-field" .Price }}
|
||||||
|
{{ template "hidden-field" .Quantity }}
|
||||||
|
{{ template "hidden-field" .Discount }}
|
||||||
|
{{ template "hidden-field" .Description }}
|
||||||
|
{{ template "hidden-select-field" .Tax }}
|
||||||
|
{{- end }}
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
<div class="invoice-data">
|
<div class="invoice-data">
|
||||||
{{ template "select-field" .Customer }}
|
{{ template "select-field" .Customer }}
|
||||||
{{ template "hidden-field" .Date }}
|
{{ template "hidden-field" .Date }}
|
||||||
|
@ -55,18 +75,19 @@
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<fieldset class="button-bar">
|
<fieldset class="button-bar">
|
||||||
<button formnovalidate formaction="{{ companyURI "/invoices" }}/{{ .Slug }}/edit"
|
<button formnovalidate
|
||||||
name="action" value="select-products"
|
name="action" value="select-products"
|
||||||
data-hx-get="{{ companyURI "/invoices/product-form" }}"
|
data-hx-get="{{ companyURI "/invoices/product-form" }}"
|
||||||
data-hx-target="#invoice-summary" data-hx-swap="beforebegin"
|
data-hx-target="#invoice-summary" data-hx-swap="beforebegin"
|
||||||
data-hx-select="unset"
|
data-hx-select="unset"
|
||||||
data-hx-vals="js:{index: document.querySelectorAll('.new-invoice-product').length}"
|
data-hx-vals="js:{index: document.querySelectorAll('.new-invoice-product').length}"
|
||||||
type="submit">{{( pgettext "Add products" "action" )}}</button>
|
type="submit">{{( pgettext "Add products" "action" )}}</button>
|
||||||
<button formnovalidate formaction="{{ companyURI "/invoices" }}/{{ .Slug }}/edit"
|
<button formnovalidate
|
||||||
id="recompute-button"
|
id="recompute-button"
|
||||||
name="action" value="update"
|
name="action" value="update"
|
||||||
type="submit">{{( pgettext "Update" "action" )}}</button>
|
type="submit">{{( pgettext "Update" "action" )}}</button>
|
||||||
<button class="primary" name="_method" value="PUT"
|
<button class="primary" name="_method" value="PUT"
|
||||||
|
formaction="{{ companyURI "/invoices" }}/{{ .Slug }}"
|
||||||
type="submit">{{( pgettext "Save" "action" )}}</button>
|
type="submit">{{( pgettext "Save" "action" )}}</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,30 @@
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newInvoicePage*/ -}}
|
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newInvoicePage*/ -}}
|
||||||
<section id="invoice-dialog-content" data-hx-target="main">
|
<section id="invoice-dialog-content" data-hx-target="main">
|
||||||
<h2>{{(pgettext "New Invoice" "title")}}</h2>
|
<h2>{{(pgettext "New Invoice" "title")}}</h2>
|
||||||
<form method="POST" action="{{ companyURI "/invoices" }}" data-hx-boost="true">
|
<form method="POST" action="{{ companyURI "/invoices/new" }}" data-hx-boost="true">
|
||||||
{{ csrfToken }}
|
{{ csrfToken }}
|
||||||
|
|
||||||
{{ with .Form -}}
|
{{ with .Form -}}
|
||||||
|
{{ if .RemovedProduct -}}
|
||||||
|
<div role="alert">
|
||||||
|
{{ with .RemovedProduct -}}
|
||||||
|
<p>{{printf (gettext "Product “%s” removed") .Name}}</p>
|
||||||
|
<button type="submit"
|
||||||
|
formnovalidate
|
||||||
|
name="action" value="restore-product"
|
||||||
|
>{{( pgettext "Undo" "action" )}}</button>
|
||||||
|
{{ template "hidden-field" .InvoiceProductId }}
|
||||||
|
{{ template "hidden-field" .ProductId }}
|
||||||
|
{{ template "hidden-field" .Name }}
|
||||||
|
{{ template "hidden-field" .Price }}
|
||||||
|
{{ template "hidden-field" .Quantity }}
|
||||||
|
{{ template "hidden-field" .Discount }}
|
||||||
|
{{ template "hidden-field" .Description }}
|
||||||
|
{{ template "hidden-select-field" .Tax }}
|
||||||
|
{{- end }}
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
<div class="invoice-data">
|
<div class="invoice-data">
|
||||||
{{ template "hidden-select-field" .InvoiceStatus }}
|
{{ template "hidden-select-field" .InvoiceStatus }}
|
||||||
{{ template "select-field" .Customer }}
|
{{ template "select-field" .Customer }}
|
||||||
|
@ -54,18 +74,19 @@
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<fieldset class="button-bar">
|
<fieldset class="button-bar">
|
||||||
<button formnovalidate formaction="{{ companyURI "/invoices/new" }}"
|
<button formnovalidate
|
||||||
name="action" value="select-products"
|
name="action" value="select-products"
|
||||||
data-hx-get="{{ companyURI "/invoices/product-form" }}"
|
data-hx-get="{{ companyURI "/invoices/product-form" }}"
|
||||||
data-hx-target="#invoice-summary" data-hx-swap="beforebegin"
|
data-hx-target="#invoice-summary" data-hx-swap="beforebegin"
|
||||||
data-hx-select="unset"
|
data-hx-select="unset"
|
||||||
data-hx-vals="js:{index: document.querySelectorAll('.new-invoice-product').length}"
|
data-hx-vals="js:{index: document.querySelectorAll('.new-invoice-product').length}"
|
||||||
type="submit">{{( pgettext "Add products" "action" )}}</button>
|
type="submit">{{( pgettext "Add products" "action" )}}</button>
|
||||||
<button formnovalidate formaction="{{ companyURI "/invoices/new" }}"
|
<button formnovalidate
|
||||||
id="recompute-button"
|
id="recompute-button"
|
||||||
name="action" value="update"
|
name="action" value="update"
|
||||||
type="submit">{{( pgettext "Update" "action" )}}</button>
|
type="submit">{{( pgettext "Update" "action" )}}</button>
|
||||||
<button class="primary" name="action" value="add"
|
<button class="primary" name="action" value="add"
|
||||||
|
formaction="{{ companyURI "/invoices" }}"
|
||||||
type="submit">{{( pgettext "Save" "action" )}}</button>
|
type="submit">{{( pgettext "Save" "action" )}}</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
Loading…
Reference in New Issue