Add the upload form to the media picker
It makes easier to upload new images from the place where we need it, instead of having to go to the media section each time. It was a little messy, this one. First of all, I realized that POSTint to /admin/media/picker to get the new media field was wrong: i was not asking the server to “accept an entity”, but only requesting a new HTML value, just like a GET to /admin/media/upload requests the form to upload a new media, thus here i should do the same, except i needed the query parameters to change the field, which is fine—it is actually a different resource, thus a different URL. Then, i thought that i could not POST the upload to /admin/media, because i returned a different HTML —the media field—, so i reused the recently unused POST to /admin/media/picker to upload that file and return the HTML for the field. It was wrong, because i was not requesting the server to put the file as a subordinate of /admin/media/picker, only /admin/media, but i did not come up with any other solution. Since i had two different upload functions now, i created uploadForm’s Handle method to refactorize the duplicated logic to a single place. Unfortunately, i did not work as i expected because uploadForm’s and mediaPicker’s MustRender methods are different, and mediaPicker has to embed uploadForm to render the form in the picker. That made me change Handle’s output to a boolean and error in order for the HTTP handler function know when to render the form with the error messages with the proper MustRender handler. However, I saw the opportunity of reusing that Handler method for editMedia, that was doing mostly the same job, but had to call a different Validate than uploadForm’s, because editMedia does not require the uploaded file. That’s when i realized that i could use an interface and that this interface could be reused not only within media but throughout the application, and added HandleMultipart in form. Had to create a different interface for multipart forms because they need different parameters in Parse that non-multipart form, when i add that interface, hence had to also change Parse to ParseForm to account for the difference in signature; not a big deal. After all that, i realized that i **could** POST to /admin/media in both cases, because i always return “an HTML entity”, it just happens that for the media section it is empty with a redirect, and for the picker is the field. That made the whole Handle method a bit redundant, but i left it nevertheless, as i find it slightly easier to read the uploadMedia function now.
This commit is contained in:
parent
7e748bcaa5
commit
b5d40cc262
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package form
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
|
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleMultipart(f MultipartForm, w http.ResponseWriter, r *http.Request, user *auth.User) (bool, error) {
|
||||||
|
if err := f.ParseMultipart(w, r); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if err := user.VerifyCSRFToken(r); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
|
_ = f.Close()
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !f.Valid(user.Locale) {
|
||||||
|
if !httplib.IsHTMxRequest(r) {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
}
|
||||||
|
_ = f.Close()
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MultipartForm interface {
|
||||||
|
io.Closer
|
||||||
|
ParseMultipart(w http.ResponseWriter, r *http.Request) error
|
||||||
|
Valid(l *locale.Locale) bool
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -49,11 +50,16 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
|
||||||
case "picker":
|
case "picker":
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
serveMediaPicker(w, r, user, company, conn)
|
serveMediaPicker(w, r, user, company, conn, newMediaPicker(r.URL.Query()))
|
||||||
case http.MethodPost:
|
|
||||||
pickMedia(w, r, user, company, conn)
|
|
||||||
default:
|
default:
|
||||||
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||||
|
}
|
||||||
|
case "field":
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
pickMedia(w, r, user, company)
|
||||||
|
default:
|
||||||
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||||
}
|
}
|
||||||
case "upload":
|
case "upload":
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
|
@ -152,72 +158,77 @@ func (page *mediaIndex) MustRender(w http.ResponseWriter, r *http.Request, user
|
||||||
template.MustRenderAdmin(w, r, user, company, "media/index.gohtml", page)
|
template.MustRenderAdmin(w, r, user, company, "media/index.gohtml", page)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveMediaPicker(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
func serveMediaPicker(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, picker *mediaPicker) {
|
||||||
media, err := collectMediaEntries(r.Context(), company, conn)
|
media, err := collectMediaEntries(r.Context(), company, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
query := r.URL.Query()
|
picker.Media = media
|
||||||
page := &mediaPicker{
|
picker.MustRender(w, r, user, company)
|
||||||
Media: media,
|
}
|
||||||
Field: &form.Media{
|
|
||||||
Input: &form.Input{
|
func pickMedia(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||||
Name: query.Get("name"),
|
picker := newMediaPicker(r.URL.Query())
|
||||||
Val: query.Get("value"),
|
picker.MustRenderField(w, r, user, company)
|
||||||
},
|
|
||||||
Label: query.Get("label"),
|
|
||||||
Prompt: query.Get("prompt"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
page.MustRender(w, r, user, company)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type mediaPicker struct {
|
type mediaPicker struct {
|
||||||
|
*uploadForm
|
||||||
Media []*mediaEntry
|
Media []*mediaEntry
|
||||||
Field *form.Media
|
Field *form.Media
|
||||||
}
|
}
|
||||||
|
|
||||||
func (page *mediaPicker) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
func newMediaPicker(query url.Values) *mediaPicker {
|
||||||
template.MustRenderNoLayout(w, r, user, company, "media/picker.gohtml", page)
|
return newMediaPickerWithUploadForm(newUploadForm(), query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pickMedia(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
func newMediaPickerWithUploadForm(f *uploadForm, query url.Values) *mediaPicker {
|
||||||
if err := r.ParseForm(); err != nil {
|
return &mediaPicker{
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
uploadForm: f,
|
||||||
return
|
Field: &form.Media{
|
||||||
}
|
Input: &form.Input{
|
||||||
input := &form.Media{
|
Name: strings.TrimSpace(query.Get("name")),
|
||||||
Input: &form.Input{
|
Val: strings.TrimSpace(query.Get("value")),
|
||||||
Name: strings.TrimSpace(r.FormValue("name")),
|
},
|
||||||
Val: strings.TrimSpace(r.FormValue("value")),
|
Label: strings.TrimSpace(query.Get("label")),
|
||||||
|
Prompt: strings.TrimSpace(query.Get("prompt")),
|
||||||
},
|
},
|
||||||
Label: strings.TrimSpace(r.FormValue("label")),
|
|
||||||
Prompt: strings.TrimSpace(r.FormValue("prompt")),
|
|
||||||
}
|
}
|
||||||
template.MustRenderNoLayout(w, r, user, company, "media/field.gohtml", input)
|
}
|
||||||
|
|
||||||
|
func (picker *mediaPicker) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||||
|
template.MustRenderNoLayout(w, r, user, company, "media/picker.gohtml", picker)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *mediaPicker) MustRenderField(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||||
|
template.MustRenderNoLayout(w, r, user, company, "media/field.gohtml", picker.Field)
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadMedia(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
func uploadMedia(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||||
f := newUploadForm()
|
f := newUploadForm()
|
||||||
if err := f.Parse(w, r); err != nil {
|
ok, err := form.HandleMultipart(f, w, r, user)
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isPicker := r.Form.Has("picker")
|
||||||
|
if !ok {
|
||||||
|
if isPicker {
|
||||||
|
serveMediaPicker(w, r, user, company, conn, newMediaPickerWithUploadForm(f, r.Form))
|
||||||
|
} else {
|
||||||
|
f.MustRender(w, r, user, company)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
if err := user.VerifyCSRFToken(r); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !f.Valid(user.Locale) {
|
|
||||||
if !httplib.IsHTMxRequest(r) {
|
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
||||||
}
|
|
||||||
f.MustRender(w, r, user, company)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bytes := f.MustReadAllFile()
|
bytes := f.MustReadAllFile()
|
||||||
conn.MustExec(r.Context(), "select add_media($1, $2, $3, $4)", company.ID, f.File.Filename(), f.File.ContentType, bytes)
|
mediaId := conn.MustGetText(r.Context(), "select add_media($1, $2, $3, $4)::text", company.ID, f.File.Filename(), f.File.ContentType, bytes)
|
||||||
httplib.Redirect(w, r, "/admin/media", http.StatusSeeOther)
|
if isPicker {
|
||||||
|
picker := newMediaPickerWithUploadForm(f, r.Form)
|
||||||
|
picker.Field.Val = mediaId
|
||||||
|
picker.MustRenderField(w, r, user, company)
|
||||||
|
} else {
|
||||||
|
httplib.Redirect(w, r, "/admin/media", http.StatusSeeOther)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type uploadForm struct {
|
type uploadForm struct {
|
||||||
|
@ -233,7 +244,7 @@ func newUploadForm() *uploadForm {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *uploadForm) Parse(w http.ResponseWriter, r *http.Request) error {
|
func (f *uploadForm) ParseMultipart(w http.ResponseWriter, r *http.Request) error {
|
||||||
maxSize := f.File.MaxSize + 1024
|
maxSize := f.File.MaxSize + 1024
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxSize)
|
||||||
if err := r.ParseMultipartForm(maxSize); err != nil {
|
if err := r.ParseMultipartForm(maxSize); err != nil {
|
||||||
|
@ -300,8 +311,8 @@ func (f *mediaForm) MustRender(w http.ResponseWriter, r *http.Request, user *aut
|
||||||
template.MustRenderAdmin(w, r, user, company, "media/form.gohtml", f)
|
template.MustRenderAdmin(w, r, user, company, "media/form.gohtml", f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *mediaForm) Parse(w http.ResponseWriter, r *http.Request) error {
|
func (f *mediaForm) ParseMultipart(w http.ResponseWriter, r *http.Request) error {
|
||||||
if err := f.uploadForm.Parse(w, r); err != nil {
|
if err := f.uploadForm.ParseMultipart(w, r); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.OriginalFilename.FillValue(r)
|
f.OriginalFilename.FillValue(r)
|
||||||
|
@ -315,19 +326,9 @@ func (f *mediaForm) Valid(l *locale.Locale) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func editMedia(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *mediaForm) {
|
func editMedia(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *mediaForm) {
|
||||||
if err := f.Parse(w, r); err != nil {
|
if ok, err := form.HandleMultipart(f, w, r, user); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
} else if !ok {
|
||||||
defer f.Close()
|
|
||||||
if err := user.VerifyCSRFToken(r); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !f.Valid(user.Locale) {
|
|
||||||
if !httplib.IsHTMxRequest(r) {
|
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
||||||
}
|
|
||||||
f.MustRender(w, r, user, company)
|
f.MustRender(w, r, user, company)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
|
@ -65,6 +66,9 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ
|
||||||
"humanizeBytes": func(bytes int64) string {
|
"humanizeBytes": func(bytes int64) string {
|
||||||
return humanizeBytes(bytes)
|
return humanizeBytes(bytes)
|
||||||
},
|
},
|
||||||
|
"queryEscape": func(s string) string {
|
||||||
|
return url.QueryEscape(s)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
templates = append(templates, "form.gohtml")
|
templates = append(templates, "form.gohtml")
|
||||||
files := make([]string, len(templates))
|
files := make([]string, len(templates))
|
||||||
|
|
53
po/ca.po
53
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: camper\n"
|
"Project-Id-Version: camper\n"
|
||||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||||
"POT-Creation-Date: 2023-09-21 01:41+0200\n"
|
"POT-Creation-Date: 2023-09-22 00:56+0200\n"
|
||||||
"PO-Revision-Date: 2023-07-22 23:45+0200\n"
|
"PO-Revision-Date: 2023-07-22 23:45+0200\n"
|
||||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||||
|
@ -591,17 +591,45 @@ msgctxt "title"
|
||||||
msgid "Media"
|
msgid "Media"
|
||||||
msgstr "Mèdia"
|
msgstr "Mèdia"
|
||||||
|
|
||||||
#: web/templates/admin/media/picker.gohtml:7
|
#: web/templates/admin/media/picker.gohtml:8
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Media Picker"
|
msgid "Media Picker"
|
||||||
msgstr "Selector de mèdia"
|
msgstr "Selector de mèdia"
|
||||||
|
|
||||||
|
#: web/templates/admin/media/picker.gohtml:19
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Upload New Media"
|
||||||
|
msgstr "Pujada de nou mèdia"
|
||||||
|
|
||||||
|
#: web/templates/admin/media/picker.gohtml:22
|
||||||
|
#: web/templates/admin/media/upload.gohtml:18
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "File"
|
||||||
|
msgstr "Fitxer"
|
||||||
|
|
||||||
#: web/templates/admin/media/picker.gohtml:26
|
#: web/templates/admin/media/picker.gohtml:26
|
||||||
|
#: web/templates/admin/media/form.gohtml:23
|
||||||
|
#: web/templates/admin/media/upload.gohtml:22
|
||||||
|
msgid "Maximum upload file size: %s"
|
||||||
|
msgstr "Mida màxima del fitxer a pujar: %s"
|
||||||
|
|
||||||
|
#: web/templates/admin/media/picker.gohtml:31
|
||||||
|
#: web/templates/admin/media/upload.gohtml:27
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Upload"
|
||||||
|
msgstr "Puja"
|
||||||
|
|
||||||
|
#: web/templates/admin/media/picker.gohtml:44
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Choose Existing Media"
|
||||||
|
msgstr "Elecció d’un mèdia existent"
|
||||||
|
|
||||||
|
#: web/templates/admin/media/picker.gohtml:55
|
||||||
#: web/templates/admin/media/index.gohtml:21
|
#: web/templates/admin/media/index.gohtml:21
|
||||||
msgid "No media uploaded yet."
|
msgid "No media uploaded yet."
|
||||||
msgstr "No s’ha pujat cap mèdia encara."
|
msgstr "No s’ha pujat cap mèdia encara."
|
||||||
|
|
||||||
#: web/templates/admin/media/picker.gohtml:29
|
#: web/templates/admin/media/picker.gohtml:58
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Canceŀla"
|
msgstr "Canceŀla"
|
||||||
|
@ -617,11 +645,6 @@ msgctxt "input"
|
||||||
msgid "Updated file"
|
msgid "Updated file"
|
||||||
msgstr "Fitxer actualitzat"
|
msgstr "Fitxer actualitzat"
|
||||||
|
|
||||||
#: web/templates/admin/media/form.gohtml:23
|
|
||||||
#: web/templates/admin/media/upload.gohtml:22
|
|
||||||
msgid "Maximum upload file size: %s"
|
|
||||||
msgstr "Mida màxima del fitxer a pujar: %s"
|
|
||||||
|
|
||||||
#: web/templates/admin/media/form.gohtml:28
|
#: web/templates/admin/media/form.gohtml:28
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Filename"
|
msgid "Filename"
|
||||||
|
@ -638,16 +661,6 @@ msgctxt "title"
|
||||||
msgid "Upload Media"
|
msgid "Upload Media"
|
||||||
msgstr "Pujada de mèdia"
|
msgstr "Pujada de mèdia"
|
||||||
|
|
||||||
#: web/templates/admin/media/upload.gohtml:18
|
|
||||||
msgctxt "input"
|
|
||||||
msgid "File"
|
|
||||||
msgstr "Fitxer"
|
|
||||||
|
|
||||||
#: web/templates/admin/media/upload.gohtml:27
|
|
||||||
msgctxt "action"
|
|
||||||
msgid "Upload"
|
|
||||||
msgstr "Puja"
|
|
||||||
|
|
||||||
#: pkg/carousel/admin.go:233 pkg/campsite/types/admin.go:236
|
#: pkg/carousel/admin.go:233 pkg/campsite/types/admin.go:236
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Cover image"
|
msgid "Cover image"
|
||||||
|
@ -797,11 +810,11 @@ msgstr "No podeu deixar el format del número de factura en blanc."
|
||||||
msgid "Cross-site request forgery detected."
|
msgid "Cross-site request forgery detected."
|
||||||
msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats."
|
msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats."
|
||||||
|
|
||||||
#: pkg/media/admin.go:255
|
#: pkg/media/admin.go:278
|
||||||
msgid "Uploaded file can not be empty."
|
msgid "Uploaded file can not be empty."
|
||||||
msgstr "No podeu deixar el fitxer del mèdia en blanc."
|
msgstr "No podeu deixar el fitxer del mèdia en blanc."
|
||||||
|
|
||||||
#: pkg/media/admin.go:314
|
#: pkg/media/admin.go:337
|
||||||
msgid "Filename can not be empty."
|
msgid "Filename can not be empty."
|
||||||
msgstr "No podeu deixar el nom del fitxer."
|
msgstr "No podeu deixar el nom del fitxer."
|
||||||
|
|
||||||
|
|
53
po/es.po
53
po/es.po
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: camper\n"
|
"Project-Id-Version: camper\n"
|
||||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||||
"POT-Creation-Date: 2023-09-21 01:41+0200\n"
|
"POT-Creation-Date: 2023-09-22 00:56+0200\n"
|
||||||
"PO-Revision-Date: 2023-07-22 23:46+0200\n"
|
"PO-Revision-Date: 2023-07-22 23:46+0200\n"
|
||||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||||
|
@ -591,17 +591,45 @@ msgctxt "title"
|
||||||
msgid "Media"
|
msgid "Media"
|
||||||
msgstr "Medios"
|
msgstr "Medios"
|
||||||
|
|
||||||
#: web/templates/admin/media/picker.gohtml:7
|
#: web/templates/admin/media/picker.gohtml:8
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Media Picker"
|
msgid "Media Picker"
|
||||||
msgstr "Selector de medio"
|
msgstr "Selector de medio"
|
||||||
|
|
||||||
|
#: web/templates/admin/media/picker.gohtml:19
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Upload New Media"
|
||||||
|
msgstr "Subida de un nuevo medio"
|
||||||
|
|
||||||
|
#: web/templates/admin/media/picker.gohtml:22
|
||||||
|
#: web/templates/admin/media/upload.gohtml:18
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "File"
|
||||||
|
msgstr "Archivo"
|
||||||
|
|
||||||
#: web/templates/admin/media/picker.gohtml:26
|
#: web/templates/admin/media/picker.gohtml:26
|
||||||
|
#: web/templates/admin/media/form.gohtml:23
|
||||||
|
#: web/templates/admin/media/upload.gohtml:22
|
||||||
|
msgid "Maximum upload file size: %s"
|
||||||
|
msgstr "Tamaño máximo del archivos a subir: %s"
|
||||||
|
|
||||||
|
#: web/templates/admin/media/picker.gohtml:31
|
||||||
|
#: web/templates/admin/media/upload.gohtml:27
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Upload"
|
||||||
|
msgstr "Subir"
|
||||||
|
|
||||||
|
#: web/templates/admin/media/picker.gohtml:44
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Choose Existing Media"
|
||||||
|
msgstr "Elección de un medio existente"
|
||||||
|
|
||||||
|
#: web/templates/admin/media/picker.gohtml:55
|
||||||
#: web/templates/admin/media/index.gohtml:21
|
#: web/templates/admin/media/index.gohtml:21
|
||||||
msgid "No media uploaded yet."
|
msgid "No media uploaded yet."
|
||||||
msgstr "No se ha subido ningún medio todavía."
|
msgstr "No se ha subido ningún medio todavía."
|
||||||
|
|
||||||
#: web/templates/admin/media/picker.gohtml:29
|
#: web/templates/admin/media/picker.gohtml:58
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Cancelar"
|
msgstr "Cancelar"
|
||||||
|
@ -617,11 +645,6 @@ msgctxt "input"
|
||||||
msgid "Updated file"
|
msgid "Updated file"
|
||||||
msgstr "Archivo actualizado"
|
msgstr "Archivo actualizado"
|
||||||
|
|
||||||
#: web/templates/admin/media/form.gohtml:23
|
|
||||||
#: web/templates/admin/media/upload.gohtml:22
|
|
||||||
msgid "Maximum upload file size: %s"
|
|
||||||
msgstr "Tamaño máximo del archivos a subir: %s"
|
|
||||||
|
|
||||||
#: web/templates/admin/media/form.gohtml:28
|
#: web/templates/admin/media/form.gohtml:28
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Filename"
|
msgid "Filename"
|
||||||
|
@ -638,16 +661,6 @@ msgctxt "title"
|
||||||
msgid "Upload Media"
|
msgid "Upload Media"
|
||||||
msgstr "Subida de medio"
|
msgstr "Subida de medio"
|
||||||
|
|
||||||
#: web/templates/admin/media/upload.gohtml:18
|
|
||||||
msgctxt "input"
|
|
||||||
msgid "File"
|
|
||||||
msgstr "Archivo"
|
|
||||||
|
|
||||||
#: web/templates/admin/media/upload.gohtml:27
|
|
||||||
msgctxt "action"
|
|
||||||
msgid "Upload"
|
|
||||||
msgstr "Subir"
|
|
||||||
|
|
||||||
#: pkg/carousel/admin.go:233 pkg/campsite/types/admin.go:236
|
#: pkg/carousel/admin.go:233 pkg/campsite/types/admin.go:236
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Cover image"
|
msgid "Cover image"
|
||||||
|
@ -797,11 +810,11 @@ msgstr "No podéis dejar el formato de número de factura en blanco."
|
||||||
msgid "Cross-site request forgery detected."
|
msgid "Cross-site request forgery detected."
|
||||||
msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados."
|
msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados."
|
||||||
|
|
||||||
#: pkg/media/admin.go:255
|
#: pkg/media/admin.go:278
|
||||||
msgid "Uploaded file can not be empty."
|
msgid "Uploaded file can not be empty."
|
||||||
msgstr "No podéis dejar el archivo del medio en blanco."
|
msgstr "No podéis dejar el archivo del medio en blanco."
|
||||||
|
|
||||||
#: pkg/media/admin.go:314
|
#: pkg/media/admin.go:337
|
||||||
msgid "Filename can not be empty."
|
msgid "Filename can not be empty."
|
||||||
msgstr "No podéis dejar el nombre del archivo en blanco."
|
msgstr "No podéis dejar el nombre del archivo en blanco."
|
||||||
|
|
||||||
|
|
|
@ -12,12 +12,9 @@ function ready(fn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ready(function () {
|
ready(function () {
|
||||||
const snackBar = Object.assign(
|
const snackBar = Object.assign(document.body.appendChild(document.createElement('section')), {
|
||||||
document.body.appendChild(document.createElement('section')),
|
id: 'snackbar',
|
||||||
{
|
});
|
||||||
id: 'snackbar',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const errorMessage = snackBar.appendChild(document.createElement('div'));
|
const errorMessage = snackBar.appendChild(document.createElement('div'));
|
||||||
errorMessage.setAttribute('role', 'alert');
|
errorMessage.setAttribute('role', 'alert');
|
||||||
|
@ -115,3 +112,25 @@ ready(function () {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function camperUploadForm(el) {
|
||||||
|
const progress = el.querySelector('progress');
|
||||||
|
htmx.on(el, 'drop', function (evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
[...evt.dataTransfer.items].forEach(function (i) {
|
||||||
|
console.log(i);
|
||||||
|
i.getAsString(console.log)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
htmx.on(el, 'dragover', function (evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
});
|
||||||
|
htmx.on(el, 'dragleave', function (evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
});
|
||||||
|
htmx.on(el, 'htmx:xhr:progress', function (evt) {
|
||||||
|
if (progress && evt.detail.lengthComputable) {
|
||||||
|
progress.setAttribute('value', evt.detail.loaded / evt.detail.total * 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
|
|
||||||
{{ define "media-picker" -}}
|
{{ define "media-picker" -}}
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/form.Media*/ -}}
|
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/form.Media*/ -}}
|
||||||
<fieldset data-hx-target="this" data-hx-swap="outerHTML" data-hx-vals='{"name": "{{ .Name }}", "value": "{{ .Val }}", "label": "{{ .Label }}", "prompt": "{{ .Prompt }}"}'>
|
<fieldset data-hx-target="this" data-hx-swap="outerHTML">
|
||||||
<legend>{{( pgettext .Label "input" )}}</legend>
|
<legend>{{( pgettext .Label "input" )}}</legend>
|
||||||
<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">
|
<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">
|
||||||
<button class="media-picker" type="button" data-hx-get="/admin/media/picker">
|
<button class="media-picker" type="button" data-hx-get="/admin/media/picker?name={{ .Name | queryEscape }}&value={{ .Val | queryEscape }}&label={{ .Label | queryEscape }}&prompt={{ .Prompt | queryEscape }}">
|
||||||
{{ if .Val -}}
|
{{ if .Val -}}
|
||||||
<img src="/admin/media/{{ .Val }}/content" alt="">
|
<img src="/admin/media/{{ .Val }}/content" alt="">
|
||||||
{{ else -}}
|
{{ else -}}
|
||||||
|
|
|
@ -2,10 +2,38 @@
|
||||||
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
<div data-hx-target="this" data-hx-swap="outerHTML">
|
<div class="media-picker" data-hx-target="this" data-hx-swap="outerHTML">
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/media.mediaPicker*/ -}}
|
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/media.mediaPicker*/ -}}
|
||||||
<h2>{{( pgettext "Media Picker" "title" )}}</h2>
|
<header>
|
||||||
<form action="/admin/media/picker" method="post" data-hx-boost="true">
|
<h3>{{( pgettext "Media Picker" "title" )}}</h3>
|
||||||
|
</header>
|
||||||
|
<form id="mediaPickerUpload" enctype="multipart/form-data" action="/admin/media" method="post"
|
||||||
|
data-hx-boost="true" data-hx-push-url="false">
|
||||||
|
{{ CSRFInput }}
|
||||||
|
{{ with .Field -}}
|
||||||
|
<input type="hidden" name="name" value="{{ .Name }}"/>
|
||||||
|
<input type="hidden" name="label" value="{{ .Label }}"/>
|
||||||
|
<input type="hidden" name="prompt" value="{{ .Prompt }}"/>
|
||||||
|
{{- end }}
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{( pgettext "Upload New Media" "title" )}}</legend>
|
||||||
|
{{ with .File -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "File" "input" )}}
|
||||||
|
<input type="file" name="{{ .Name }}"
|
||||||
|
required {{ template "error-attrs" . }}><br>
|
||||||
|
</label>
|
||||||
|
<p>{{ printf (gettext "Maximum upload file size: %s") (.MaxSize | humanizeBytes) }}</p>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
{{- end }}
|
||||||
|
</fieldset>
|
||||||
|
<footer>
|
||||||
|
<button name="picker" type="submit">{{( pgettext "Upload" "action" )}}</button>
|
||||||
|
<progress value="0" max="100"></progress>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
<script>camperUploadForm(document.getElementById('mediaPickerUpload'))</script>
|
||||||
|
<form action="/admin/media/field" method="get" data-hx-boost="true" data-hx-push-url="false">
|
||||||
{{ with .Field -}}
|
{{ with .Field -}}
|
||||||
<input type="hidden" name="name" value="{{ .Name }}"/>
|
<input type="hidden" name="name" value="{{ .Name }}"/>
|
||||||
<input type="hidden" name="label" value="{{ .Label }}"/>
|
<input type="hidden" name="label" value="{{ .Label }}"/>
|
||||||
|
@ -13,6 +41,7 @@
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{ if .Media -}}
|
{{ if .Media -}}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
<legend>{{( pgettext "Choose Existing Media" "title" )}}</legend>
|
||||||
<ul class="media-grid">
|
<ul class="media-grid">
|
||||||
{{ range .Media -}}
|
{{ range .Media -}}
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
{{ define "content" -}}
|
{{ define "content" -}}
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/media.uploadForm*/ -}}
|
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/media.uploadForm*/ -}}
|
||||||
{{ template "settings-tabs" "media" }}
|
{{ template "settings-tabs" "media" }}
|
||||||
<form id="upload" enctype="multipart/form-data" action="/admin/media" method="post" data-hx-boost="true">
|
<form id="mediaUpload" enctype="multipart/form-data" action="/admin/media" method="post" data-hx-boost="true">
|
||||||
<h2>{{( pgettext "Upload Media" "title" )}}</h2>
|
<h2>{{( pgettext "Upload Media" "title" )}}</h2>
|
||||||
{{ CSRFInput }}
|
{{ CSRFInput }}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
@ -28,24 +28,5 @@
|
||||||
<progress value="0" max="100"></progress>
|
<progress value="0" max="100"></progress>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
<script>
|
<script>camperUploadForm(document.getElementById('mediaUpload'))</script>
|
||||||
htmx.on('#upload', 'drop', function (evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
[...evt.dataTransfer.items].forEach(function (i) {
|
|
||||||
console.log(i);
|
|
||||||
i.getAsString(console.log)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
htmx.on('#upload', 'dragover', function (evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
});
|
|
||||||
htmx.on('#upload', 'dragleave', function (evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
});
|
|
||||||
htmx.on('#upload', 'htmx:xhr:progress', function (evt) {
|
|
||||||
if (evt.detail.lengthComputable) {
|
|
||||||
htmx.find('progress').setAttribute('value', evt.detail.loaded / evt.detail.total * 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
Loading…
Reference in New Issue