diff --git a/server/perkeepd/ui/goui/downloadbutton/downloadbutton.go b/server/perkeepd/ui/goui/downloadbutton/downloadbutton.go deleted file mode 100644 index 2de69b2f2..000000000 --- a/server/perkeepd/ui/goui/downloadbutton/downloadbutton.go +++ /dev/null @@ -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 -} diff --git a/server/perkeepd/ui/goui/main.go b/server/perkeepd/ui/goui/main.go index 498ccebd6..192e4df8e 100644 --- a/server/perkeepd/ui/goui/main.go +++ b/server/perkeepd/ui/goui/main.go @@ -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. diff --git a/server/perkeepd/ui/index.js b/server/perkeepd/ui/index.js index a0f1466a0..bc0958146 100644 --- a/server/perkeepd/ui/index.js +++ b/server/perkeepd/ui/index.js @@ -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() {