diff --git a/browser/app/js/objects/ObjectActions.js b/browser/app/js/objects/ObjectActions.js
index a1660a630..2915eb426 100644
--- a/browser/app/js/objects/ObjectActions.js
+++ b/browser/app/js/objects/ObjectActions.js
@@ -47,6 +47,11 @@ export class ObjectActions extends React.Component {
SHARE_OBJECT_EXPIRY_MINUTES
)
}
+ handleDownload(e) {
+ e.preventDefault()
+ const { object, downloadObject } = this.props
+ downloadObject(object.name)
+ }
deleteObject() {
const { object, deleteObject } = this.props
deleteObject(object.name)
@@ -82,6 +87,7 @@ export class ObjectActions extends React.Component {
@@ -90,6 +96,7 @@ export class ObjectActions extends React.Component {
@@ -98,6 +105,15 @@ export class ObjectActions extends React.Component {
+
+
+
@@ -134,6 +150,7 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = (dispatch) => {
return {
+ downloadObject: object => dispatch(objectsActions.downloadObject(object)),
shareObject: (object, days, hours, minutes) =>
dispatch(objectsActions.shareObject(object, days, hours, minutes)),
deleteObject: (object) => dispatch(objectsActions.deleteObject(object)),
diff --git a/browser/app/js/objects/PrefixActions.js b/browser/app/js/objects/PrefixActions.js
new file mode 100644
index 000000000..b6ebcb0e7
--- /dev/null
+++ b/browser/app/js/objects/PrefixActions.js
@@ -0,0 +1,95 @@
+/*
+ * MinIO Cloud Storage (C) 2020 MinIO, Inc.
+ *
+ * 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.
+ */
+
+import React from "react"
+import { connect } from "react-redux"
+import { Dropdown } from "react-bootstrap"
+import DeleteObjectConfirmModal from "./DeleteObjectConfirmModal"
+import * as actions from "./actions"
+
+export class PrefixActions extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ showDeleteConfirmation: false,
+ }
+ }
+ handleDownload(e) {
+ e.preventDefault()
+ const { object, downloadPrefix } = this.props
+ downloadPrefix(object.name)
+ }
+ deleteObject() {
+ const { object, deleteObject } = this.props
+ deleteObject(object.name)
+ }
+ showDeleteConfirmModal(e) {
+ e.preventDefault()
+ this.setState({ showDeleteConfirmation: true })
+ }
+ hideDeleteConfirmModal() {
+ this.setState({
+ showDeleteConfirmation: false,
+ })
+ }
+ render() {
+ const { object, showShareObjectModal, shareObjectName } = this.props
+ return (
+
+
+
+
+
+
+
+
+
+
+ {this.state.showDeleteConfirmation && (
+
+ )}
+
+ )
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ object: ownProps.object,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ downloadPrefix: object => dispatch(actions.downloadPrefix(object)),
+ deleteObject: (object) => dispatch(actions.deleteObject(object)),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(PrefixActions)
diff --git a/browser/app/js/objects/PrefixContainer.js b/browser/app/js/objects/PrefixContainer.js
index d88fc894e..5f8ccfb7b 100644
--- a/browser/app/js/objects/PrefixContainer.js
+++ b/browser/app/js/objects/PrefixContainer.js
@@ -17,22 +17,32 @@
import React from "react"
import { connect } from "react-redux"
import ObjectItem from "./ObjectItem"
+import PrefixActions from "./PrefixActions"
import * as actionsObjects from "./actions"
+import { getCheckedList } from "./selectors"
-export const PrefixContainer = ({ object, currentPrefix, selectPrefix }) => {
+export const PrefixContainer = ({
+ object,
+ currentPrefix,
+ checkedObjectsCount,
+ selectPrefix
+}) => {
const props = {
name: object.name,
contentType: object.contentType,
onClick: () => selectPrefix(`${currentPrefix}${object.name}`)
}
-
+ if (checkedObjectsCount == 0) {
+ props.actionButtons =
+ }
return
}
const mapStateToProps = (state, ownProps) => {
return {
object: ownProps.object,
- currentPrefix: state.objects.currentPrefix
+ currentPrefix: state.objects.currentPrefix,
+ checkedObjectsCount: getCheckedList(state).length
}
}
diff --git a/browser/app/js/objects/__tests__/ObjectActions.test.js b/browser/app/js/objects/__tests__/ObjectActions.test.js
index 7339b1a5d..0f45bb881 100644
--- a/browser/app/js/objects/__tests__/ObjectActions.test.js
+++ b/browser/app/js/objects/__tests__/ObjectActions.test.js
@@ -67,6 +67,20 @@ describe("ObjectActions", () => {
})
+ it("should call downloadObject when single object is selected and download button is clicked", () => {
+ const downloadObject = jest.fn()
+ const wrapper = shallow(
+
+ )
+ wrapper
+ .find("a")
+ .at(1)
+ .simulate("click", { preventDefault: jest.fn() })
+ expect(downloadObject).toHaveBeenCalled()
+ })
it("should show PreviewObjectModal when preview action is clicked", () => {
@@ -106,7 +120,7 @@ describe("ObjectActions", () => {
)
expect(wrapper
.find("a")
- .length).toBe(2) // find only the other 2
+ .length).toBe(3) // find only the other 2
})
it("should call shareObject with object and expiry", () => {
diff --git a/browser/app/js/objects/__tests__/PrefixActions.test.js b/browser/app/js/objects/__tests__/PrefixActions.test.js
new file mode 100644
index 000000000..82befde4a
--- /dev/null
+++ b/browser/app/js/objects/__tests__/PrefixActions.test.js
@@ -0,0 +1,84 @@
+/*
+ * MinIO Cloud Storage (C) 2018 MinIO, Inc.
+ *
+ * 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.
+ */
+
+import React from "react"
+import { shallow } from "enzyme"
+import { PrefixActions } from "../PrefixActions"
+
+describe("PrefixActions", () => {
+ it("should render without crashing", () => {
+ shallow()
+ })
+
+ it("should show DeleteObjectConfirmModal when delete action is clicked", () => {
+ const wrapper = shallow(
+
+ )
+ wrapper
+ .find("a")
+ .last()
+ .simulate("click", { preventDefault: jest.fn() })
+ expect(wrapper.state("showDeleteConfirmation")).toBeTruthy()
+ expect(wrapper.find("DeleteObjectConfirmModal").length).toBe(1)
+ })
+
+ it("should hide DeleteObjectConfirmModal when Cancel button is clicked", () => {
+ const wrapper = shallow(
+
+ )
+ wrapper
+ .find("a")
+ .last()
+ .simulate("click", { preventDefault: jest.fn() })
+ wrapper.find("DeleteObjectConfirmModal").prop("hideDeleteConfirmModal")()
+ wrapper.update()
+ expect(wrapper.state("showDeleteConfirmation")).toBeFalsy()
+ expect(wrapper.find("DeleteObjectConfirmModal").length).toBe(0)
+ })
+
+ it("should call deleteObject with object name", () => {
+ const deleteObject = jest.fn()
+ const wrapper = shallow(
+
+ )
+ wrapper
+ .find("a")
+ .last()
+ .simulate("click", { preventDefault: jest.fn() })
+ wrapper.find("DeleteObjectConfirmModal").prop("deleteObject")()
+ expect(deleteObject).toHaveBeenCalledWith("abc/")
+ })
+
+
+ it("should call downloadPrefix when single object is selected and download button is clicked", () => {
+ const downloadPrefix = jest.fn()
+ const wrapper = shallow(
+
+ )
+ wrapper
+ .find("a")
+ .first()
+ .simulate("click", { preventDefault: jest.fn() })
+ expect(downloadPrefix).toHaveBeenCalled()
+ })
+})
diff --git a/browser/app/js/objects/__tests__/PrefixContainer.test.js b/browser/app/js/objects/__tests__/PrefixContainer.test.js
index fef61a186..299615811 100644
--- a/browser/app/js/objects/__tests__/PrefixContainer.test.js
+++ b/browser/app/js/objects/__tests__/PrefixContainer.test.js
@@ -41,4 +41,22 @@ describe("PrefixContainer", () => {
wrapper.find("Connect(ObjectItem)").prop("onClick")()
expect(selectPrefix).toHaveBeenCalledWith("xyz/abc/")
})
+
+ it("should pass actions to ObjectItem", () => {
+ const wrapper = shallow(
+
+ )
+ expect(wrapper.find("Connect(ObjectItem)").prop("actionButtons")).not.toBe(
+ undefined
+ )
+ })
+
+ it("should pass empty actions to ObjectItem when checkedObjectCount is more than 0", () => {
+ const wrapper = shallow(
+
+ )
+ expect(wrapper.find("Connect(ObjectItem)").prop("actionButtons")).toBe(
+ undefined
+ )
+ })
})
diff --git a/browser/app/js/objects/__tests__/actions.test.js b/browser/app/js/objects/__tests__/actions.test.js
index 421ebc6f4..e667f5aea 100644
--- a/browser/app/js/objects/__tests__/actions.test.js
+++ b/browser/app/js/objects/__tests__/actions.test.js
@@ -68,6 +68,9 @@ jest.mock("../../web", () => ({
.mockImplementationOnce(() => {
return Promise.reject({ message: "Error in creating token" })
})
+ .mockImplementationOnce(() => {
+ return Promise.resolve({ token: "test" })
+ })
.mockImplementationOnce(() => {
return Promise.resolve({ token: "test" })
}),
@@ -485,6 +488,34 @@ describe("Objects actions", () => {
})
})
+ it("should download prefix", () => {
+ const open = jest.fn()
+ const send = jest.fn()
+ const xhrMockClass = () => ({
+ open: open,
+ send: send
+ })
+ window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass)
+
+ const store = mockStore({
+ buckets: { currentBucket: "bk1" },
+ objects: { currentPrefix: "pre1/" }
+ })
+ return store.dispatch(actionsObjects.downloadPrefix("pre2/")).then(() => {
+ const requestUrl = `${
+ location.origin
+ }${minioBrowserPrefix}/zip?token=test`
+ expect(open).toHaveBeenCalledWith("POST", requestUrl, true)
+ expect(send).toHaveBeenCalledWith(
+ JSON.stringify({
+ bucketName: "bk1",
+ prefix: "pre1/",
+ objects: ["pre2/"]
+ })
+ )
+ })
+ })
+
it("creates objects/CHECKED_LIST_ADD action", () => {
const store = mockStore()
const expectedActions = [
diff --git a/browser/app/js/objects/actions.js b/browser/app/js/objects/actions.js
index 8e71d2d1f..4c6eff340 100644
--- a/browser/app/js/objects/actions.js
+++ b/browser/app/js/objects/actions.js
@@ -360,6 +360,19 @@ export const downloadObject = (object) => {
}
}
+export const downloadPrefix = (object) => {
+ return function (dispatch, getState) {
+ return downloadObjects(
+ getCurrentBucket(getState()),
+ getCurrentPrefix(getState()),
+ [object],
+ `${object.slice(0, -1)}.zip`,
+ dispatch
+ )
+ }
+}
+
+
export const checkObject = (object) => ({
type: CHECKED_LIST_ADD,
object,
@@ -376,21 +389,28 @@ export const resetCheckedList = () => ({
export const downloadCheckedObjects = () => {
return function (dispatch, getState) {
- const state = getState()
+ return downloadObjects(
+ getCurrentBucket(getState()),
+ getCurrentPrefix(getState()),
+ getCheckedList(getState()),
+ null,
+ dispatch
+ )
+ }
+}
+
+const downloadObjects = (bucketName, prefix, objects, filename, dispatch) => {
const req = {
- bucketName: getCurrentBucket(state),
- prefix: getCurrentPrefix(state),
- objects: getCheckedList(state),
+ bucketName: bucketName,
+ prefix: prefix,
+ objects: objects,
}
- if (!web.LoggedIn()) {
- const requestUrl = location.origin + "/minio/zip?token="
- downloadZip(requestUrl, req, dispatch)
- } else {
+ if (web.LoggedIn()) {
return web
.CreateURLToken()
.then((res) => {
const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=${res.token}`
- downloadZip(requestUrl, req, dispatch)
+ downloadZip(requestUrl, req, filename, dispatch)
})
.catch((err) =>
dispatch(
@@ -400,11 +420,13 @@ export const downloadCheckedObjects = () => {
})
)
)
+ } else {
+ const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=`
+ downloadZip(requestUrl, req, filename, dispatch)
}
- }
}
-const downloadZip = (url, req, dispatch) => {
+const downloadZip = (url, req, filename, dispatch) => {
var anchor = document.createElement("a")
document.body.appendChild(anchor)
@@ -422,7 +444,7 @@ const downloadZip = (url, req, dispatch) => {
var separator = req.prefix.length > 1 ? "-" : ""
anchor.href = blobUrl
- anchor.download =
+ anchor.download = filename ||
req.bucketName + separator + req.prefix.slice(0, -1) + ".zip"
anchor.click()