downloadbutton: migrate from goui to JS

This commit is contained in:
aviau 2021-01-10 21:19:30 -05:00 committed by Alexandre Viau
parent 39bc87d273
commit 1700b5be5f
3 changed files with 58 additions and 200 deletions

View File

@ -1,163 +0,0 @@
/*
Copyright 2017 The Perkeep Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package downloadbutton provides a Button element that is used in the sidebar of
// the web UI, to download as a zip file all selected files.
package downloadbutton
import (
"fmt"
"strings"
"github.com/gopherjs/gopherjs/js"
"perkeep.org/pkg/blob"
"honnef.co/go/js/dom"
"myitcv.io/react"
)
//go:generate reactGen
// New returns the button element. It should be used as the entry point, to
// create the needed React element.
//
// key is the id for when the button is in a list, see
// https://facebook.github.io/react/docs/lists-and-keys.html
//
// config is the web UI config that was fetched from the server.
//
// cbs is a wrapper around the callback functions required by this component.
func New(key string, config map[string]string, cbs *Callbacks) react.Element {
if config == nil {
fmt.Println("Nil config for DownloadItemsBtn")
return nil
}
downloadHelper, ok := config["downloadHelper"]
if !ok {
fmt.Println("No downloadHelper in config for DownloadItemsBtn")
return nil
}
if cbs == nil {
fmt.Println("Nil callbacks for DownloadItemsBtn")
return nil
}
if cbs.GetSelection == nil {
fmt.Println("Nil getSelection callback for DownloadItemsBtn")
return nil
}
if key == "" {
// A key is only needed in the context of a list, which is why
// it is up to the caller to choose it. Just creating it here for
// the sake of consistency.
key = "downloadItemsButton"
}
props := DownloadItemsBtnProps{
Key: key,
Callbacks: cbs,
downloadHelper: downloadHelper,
}
return buildDownloadItemsBtnElem(props)
}
// Callbacks defines the callbacks that must be provided when creating a
// DownloadItemsBtn instance.
type Callbacks struct {
o *js.Object
// GetSelection returns the list of files (blobRefs) selected
// for downloading.
GetSelection func() []string `js:"getSelection"`
}
// DownloadItemsBtnDef is the definition for the button, that Renders as a React
// Button.
type DownloadItemsBtnDef struct {
react.ComponentDef
}
type DownloadItemsBtnProps struct {
// Key is the id for when the button is in a list, see
// https://facebook.github.io/react/docs/lists-and-keys.html
Key string
*Callbacks
downloadHelper string
}
func (d DownloadItemsBtnDef) Render() react.Element {
return react.Button(
&react.ButtonProps{
Key: d.Props().Key,
OnClick: d,
},
react.S("Download"),
)
}
func (d DownloadItemsBtnDef) OnClick(*react.SyntheticMouseEvent) {
// Note: there's a "memleak", as in: until the selection is cleared and
// another one is started, this button stays allocated. It is of no
// consequence in this case as we don't allocate a lot for this element (in
// previous experiments where the zip archive was in memory, the leak was
// definitely noticeable then), but it is something to keep in mind for
// future elements.
go func() {
if err := d.downloadSelection(); err != nil {
dom.GetWindow().Alert(fmt.Sprintf("%v", err))
}
}()
}
func (d DownloadItemsBtnDef) downloadSelection() error {
selection := d.Props().Callbacks.GetSelection()
downloadPrefix := d.Props().downloadHelper
fileRefs := []string{}
for _, file := range selection {
ref, ok := blob.Parse(file)
if !ok {
return fmt.Errorf("cannot download %q, not a valid blobRef", file)
}
fileRefs = append(fileRefs, ref.String())
}
if len(fileRefs) < 2 {
// Do not ask for a zip if we only want one file
dom.GetWindow().Open(fmt.Sprintf("%s/%s", downloadPrefix, fileRefs[0]), "", "")
return nil
}
el := dom.GetWindow().Document().CreateElement("input")
input := el.(*dom.HTMLInputElement)
input.Type = "text"
input.Name = "files"
input.Value = strings.Join(fileRefs, ",")
el = dom.GetWindow().Document().CreateElement("form")
form := el.(*dom.HTMLFormElement)
form.Action = downloadPrefix
form.Method = "POST"
form.AppendChild(input)
// As per
// https://html.spec.whatwg.org/multipage/forms.html#form-submission-algorithm
// step 2., a form must be connected to the DOM for submission.
body := dom.GetWindow().Document().QuerySelector("body")
body.AppendChild(form)
defer body.RemoveChild(form)
form.Submit()
return nil
}

View File

@ -24,7 +24,6 @@ import (
"perkeep.org/pkg/blob"
"perkeep.org/server/perkeepd/ui/goui/aboutdialog"
"perkeep.org/server/perkeepd/ui/goui/dirchildren"
"perkeep.org/server/perkeepd/ui/goui/downloadbutton"
"perkeep.org/server/perkeepd/ui/goui/importshare"
"perkeep.org/server/perkeepd/ui/goui/mapquery"
"perkeep.org/server/perkeepd/ui/goui/selectallbutton"
@ -35,11 +34,10 @@ import (
func main() {
js.Global.Set("goreact", map[string]interface{}{
"AboutMenuItem": aboutdialog.New,
"DownloadItemsBtn": downloadbutton.New,
"ShareItemsBtn": sharebutton.New,
"SelectAllBtn": selectallbutton.New,
"NewDirChildren": dirchildren.New,
"AboutMenuItem": aboutdialog.New,
"ShareItemsBtn": sharebutton.New,
"SelectAllBtn": selectallbutton.New,
"NewDirChildren": dirchildren.New,
// TODO: we want to investigate integrating the share importer with the other
// importers. But if we instead end up keeping it tied to a dialog, we need to add
// a cancel button to the dialog, that triggers the context cancellation.

View File

@ -1010,6 +1010,53 @@ cam.IndexPage = React.createClass({
goog.Promise.all(changes).then(this.refreshIfNecessary_);
},
handleDownload_: function() {
var files = [];
goog.object.getKeys(this.state.selection).forEach(function(br) {
var meta = this.childSearchSession_.getResolvedMeta(br);
if (!meta) {
return;
}
if (!meta.file && !meta.dir) {
// br does not have a file or a directory description, so it's probably neither.
return;
}
if (meta.file && !meta.file.fileName) {
// looks like a file, but no file name
return;
}
if (meta.dir && !meta.dir.fileName) {
// looks like a dir, but no file name
return;
}
files.push(meta.blobRef);
}.bind(this));
var downloadPrefix = this.props.config.downloadHelper;
if (files.length < 2) {
window.open(`${downloadPrefix}/${files[0]}`);
}
var input = document.createElement("input");
input.type = "text";
input.name = "files";
input.value = files.join(",");
var form = document.createElement("form");
form.action = downloadPrefix;
form.method = "POST";
form.appendChild(input);
// As per
// https://html.spec.whatwg.org/multipage/forms.html#form-submission-algorithm
// step 2., a form must be connected to the DOM for submission.
var body = document.querySelector("body");
body.appendChild(form);
form.submit();
body.removeChild(form);
},
handleOpenWindow_: function(url) {
this.props.openWindow(url);
},
@ -1301,37 +1348,13 @@ cam.IndexPage = React.createClass({
},
getDownloadSelectionItem_: function() {
var callbacks = {};
// TODO(mpl): I'm doing the selection business in javascript for now,
// since we already have the search session results handy.
// It shouldn't be any problem to move it to Go later.
callbacks.getSelection = function() {
var selection = goog.object.getKeys(this.state.selection);
var files = [];
selection.forEach(function(br) {
var meta = this.childSearchSession_.getResolvedMeta(br);
if (!meta) {
return;
}
if (!meta.file && !meta.dir) {
// br does not have a file or a directory description, so it's probably neither.
return;
}
if (meta.file && !meta.file.fileName) {
// looks like a file, but no file name
return;
}
if (meta.dir && !meta.dir.fileName) {
// looks like a dir, but no file name
return;
}
files.push(meta.blobRef);
}.bind(this))
return files;
}.bind(this);
return goreact.DownloadItemsBtn('donwloadBtnSidebar', this.props.config, callbacks);
return React.DOM.button(
{
key: "download",
onClick: this.handleDownload_,
},
"Download",
)
},
getShareSelectionItem_: function() {