mirror of https://github.com/perkeep/perkeep.git
Merge "server/camlistored: add "Select all" button to web UI"
This commit is contained in:
commit
ef4af89ea4
|
@ -23,6 +23,7 @@ import (
|
|||
"perkeep.org/server/camlistored/ui/goui/downloadbutton"
|
||||
"perkeep.org/server/camlistored/ui/goui/geo"
|
||||
"perkeep.org/server/camlistored/ui/goui/mapquery"
|
||||
"perkeep.org/server/camlistored/ui/goui/selectallbutton"
|
||||
"perkeep.org/server/camlistored/ui/goui/sharebutton"
|
||||
|
||||
"github.com/gopherjs/gopherjs/js"
|
||||
|
@ -33,6 +34,7 @@ func main() {
|
|||
"AboutMenuItem": aboutdialog.New,
|
||||
"DownloadItemsBtn": downloadbutton.New,
|
||||
"ShareItemsBtn": sharebutton.New,
|
||||
"SelectAllBtn": selectallbutton.New,
|
||||
"Geocode": geo.Lookup,
|
||||
"IsLocPredicate": geo.IsLocPredicate,
|
||||
"HandleLocAreaPredicate": geo.HandleLocAreaPredicate,
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
//go:generate reactGen
|
||||
|
||||
/*
|
||||
Copyright 2018 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 selectallbutton provides a Button element that is used in the sidebar of
|
||||
// the web UI, to select all the items matching the current search in the web UI.
|
||||
// The button is disabled if the current search is not an explicit predicate
|
||||
// search (e.g. "tag:foo"), or a container predicate (ref:<blobref>).
|
||||
package selectallbutton
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"perkeep.org/pkg/auth"
|
||||
"perkeep.org/pkg/blob"
|
||||
"perkeep.org/pkg/client"
|
||||
"perkeep.org/pkg/search"
|
||||
|
||||
"github.com/gopherjs/gopherjs/js"
|
||||
"honnef.co/go/js/dom"
|
||||
"myitcv.io/react"
|
||||
)
|
||||
|
||||
// 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 cbs == nil {
|
||||
fmt.Println("Nil callbacks for SelectAllBtn")
|
||||
return nil
|
||||
}
|
||||
if cbs.GetQuery == nil {
|
||||
fmt.Println("Nil GetQuery callback for SelectAllBtn")
|
||||
return nil
|
||||
}
|
||||
if cbs.GetQuery() == "" {
|
||||
// This makes sure we don't enable the button when we're on the "main" page,
|
||||
// with no search.
|
||||
return nil
|
||||
}
|
||||
if cbs.SetSelection == nil {
|
||||
fmt.Println("Nil SetSelection callback for SelectAllBtn")
|
||||
return nil
|
||||
}
|
||||
if config == nil {
|
||||
fmt.Println("Nil config for SelectAllBtn")
|
||||
return nil
|
||||
}
|
||||
authToken, ok := config["authToken"]
|
||||
if !ok {
|
||||
fmt.Println("No authToken in config for SelectAllBtn")
|
||||
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 = "selectAllButton"
|
||||
}
|
||||
props := SelectAllBtnProps{
|
||||
key: key,
|
||||
callbacks: cbs,
|
||||
authToken: authToken,
|
||||
}
|
||||
return SelectAllBtn(props)
|
||||
}
|
||||
|
||||
// Callbacks defines the callbacks that must be provided when creating a
|
||||
// SelectAllBtn instance.
|
||||
type Callbacks struct {
|
||||
o *js.Object
|
||||
|
||||
// GetQuery returns the current search session predicate.
|
||||
GetQuery func() string `js:"getQuery"`
|
||||
|
||||
// SetSelection sets the given selection of blobRefs as the selected permanodes
|
||||
// in the web UI.
|
||||
SetSelection func(map[string]bool) `js:"setSelection"`
|
||||
}
|
||||
|
||||
// SelectAllBtnDef defines a React button to select all items matching the
|
||||
// current search query.
|
||||
type SelectAllBtnDef struct {
|
||||
react.ComponentDef
|
||||
}
|
||||
|
||||
type SelectAllBtnProps 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 *Callbacks
|
||||
|
||||
authToken string
|
||||
}
|
||||
|
||||
func SelectAllBtn(p SelectAllBtnProps) *SelectAllBtnElem {
|
||||
return buildSelectAllBtnElem(p)
|
||||
}
|
||||
|
||||
func (d SelectAllBtnDef) Render() react.Element {
|
||||
return react.Button(
|
||||
&react.ButtonProps{
|
||||
OnClick: d,
|
||||
Key: d.Props().key,
|
||||
},
|
||||
react.S("Select all"),
|
||||
)
|
||||
}
|
||||
|
||||
func (d SelectAllBtnDef) OnClick(*react.SyntheticMouseEvent) {
|
||||
go func() {
|
||||
selection, err := d.findAll()
|
||||
if err != nil {
|
||||
dom.GetWindow().Alert(fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
d.Props().callbacks.SetSelection(selection)
|
||||
}()
|
||||
}
|
||||
|
||||
// getQuery returns the query corresponding to the current search in the web UI.
|
||||
func (d SelectAllBtnDef) getQuery() *search.SearchQuery {
|
||||
predicate := d.Props().callbacks.GetQuery()
|
||||
if !strings.HasPrefix(predicate, "ref:") {
|
||||
return &search.SearchQuery{
|
||||
Limit: -1,
|
||||
Expression: predicate,
|
||||
}
|
||||
}
|
||||
|
||||
// If we've got a ref: predicate, assume the given blobRef is a container, and
|
||||
// find its children.
|
||||
blobRef := strings.TrimPrefix(predicate, "ref:")
|
||||
br, ok := blob.Parse(blobRef)
|
||||
if !ok {
|
||||
println(`Invalid blobRef in "ref:" predicate: ` + blobRef)
|
||||
return nil
|
||||
}
|
||||
return &search.SearchQuery{
|
||||
Limit: -1,
|
||||
Constraint: &search.Constraint{Permanode: &search.PermanodeConstraint{
|
||||
Relation: &search.RelationConstraint{
|
||||
Relation: "parent",
|
||||
Any: &search.Constraint{
|
||||
BlobRefPrefix: br.String(),
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// findAll returns all the permanodes matching the current web UI search
|
||||
// session. The javascript UI code uses a javascript object with blobRefs as
|
||||
// properties to represent a user's selection of permanodes. Since gopherjs
|
||||
// converts a Go map to a javascript object, findAll returns such a map so it
|
||||
// matches directly what the UI code wants as a selection object.
|
||||
func (d SelectAllBtnDef) findAll() (map[string]bool, error) {
|
||||
query := d.getQuery()
|
||||
if query == nil {
|
||||
return nil, nil
|
||||
}
|
||||
authToken := d.Props().authToken
|
||||
am, err := auth.NewTokenAuth(authToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error setting up auth: %v", err)
|
||||
}
|
||||
cl := newClient(am)
|
||||
res, err := cl.Query(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blobs := make(map[string]bool, len(res.Blobs))
|
||||
for _, v := range res.Blobs {
|
||||
blobs[v.Blob.String()] = true
|
||||
}
|
||||
return blobs, nil
|
||||
}
|
||||
|
||||
func newClient(am auth.AuthMode) *client.Client {
|
||||
cl := client.NewFromParams("", am, client.OptionSameOrigin(true))
|
||||
// Here we force the use of the http.DefaultClient. Otherwise, we'll hit
|
||||
// one of the net.Dial* calls due to custom transport we set up by default
|
||||
// in pkg/client. Which we don't want because system calls are prohibited by
|
||||
// gopherjs.
|
||||
cl.SetHTTPClient(nil)
|
||||
return cl
|
||||
}
|
|
@ -1148,43 +1148,61 @@ cam.IndexPage = React.createClass({
|
|||
// 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.dir) {
|
||||
files.push({'blobRef': meta.blobRef, 'isDir': 'true'});
|
||||
return;
|
||||
}
|
||||
if (meta.file) {
|
||||
files.push({'blobRef': meta.blobRef, 'isDir': 'false'});
|
||||
return;
|
||||
}
|
||||
}.bind(this))
|
||||
return files;
|
||||
}.bind(this);
|
||||
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.dir) {
|
||||
files.push({'blobRef': meta.blobRef, 'isDir': 'true'});
|
||||
return;
|
||||
}
|
||||
if (meta.file) {
|
||||
files.push({'blobRef': meta.blobRef, 'isDir': 'false'});
|
||||
return;
|
||||
}
|
||||
}.bind(this))
|
||||
return files;
|
||||
}.bind(this);
|
||||
|
||||
callbacks.showSharedURL = function(sharedURL, anchorText) {
|
||||
// TODO(mpl): Port the dialog to Go.
|
||||
this.setState({
|
||||
messageDialogVisible: true,
|
||||
messageDialogContents: React.DOM.div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
position: 'relative',
|
||||
},},
|
||||
React.DOM.div({}, 'Share URL:'),
|
||||
React.DOM.div({}, React.DOM.a({href: sharedURL}, anchorText))
|
||||
),
|
||||
});
|
||||
}.bind(this);
|
||||
// TODO(mpl): Port the dialog to Go.
|
||||
this.setState({
|
||||
messageDialogVisible: true,
|
||||
messageDialogContents: React.DOM.div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
position: 'relative',
|
||||
},},
|
||||
React.DOM.div({}, 'Share URL:'),
|
||||
React.DOM.div({}, React.DOM.a({href: sharedURL}, anchorText))
|
||||
),
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
return goreact.ShareItemsBtn('shareBtnSidebar', this.props.config, callbacks);
|
||||
},
|
||||
|
||||
getSelectAllItem_: function() {
|
||||
var callbacks = {};
|
||||
callbacks.getQuery = function() {
|
||||
var target = this.getTargetBlobref_();
|
||||
var query = '';
|
||||
if (target) {
|
||||
query = 'ref:' + target;
|
||||
} else {
|
||||
query = this.state.currentURL.getParameterValue('q') || '';
|
||||
}
|
||||
return query;
|
||||
}.bind(this);
|
||||
callbacks.setSelection = function(selection) {
|
||||
this.setSelection_(selection);
|
||||
}.bind(this);
|
||||
return goreact.SelectAllBtn('selectallBtnSidebar', this.props.config, callbacks);
|
||||
},
|
||||
|
||||
getSidebar_: function(selectedAspect) {
|
||||
if (selectedAspect) {
|
||||
if (selectedAspect.fragment == 'search' || selectedAspect.fragment == 'contents') {
|
||||
|
@ -1204,6 +1222,7 @@ cam.IndexPage = React.createClass({
|
|||
}
|
||||
].filter(goog.functions.identity),
|
||||
selectionControls: [
|
||||
this.getSelectAllItem_(),
|
||||
this.getClearSelectionItem_(),
|
||||
this.getCreateSetWithSelectionItem_(),
|
||||
this.getSelectAsCurrentSetItem_(),
|
||||
|
|
Loading…
Reference in New Issue