diff --git a/browser/app/js/browser/MainContent.js b/browser/app/js/browser/MainContent.js index 08f8bfb0d..6fa052fc4 100644 --- a/browser/app/js/browser/MainContent.js +++ b/browser/app/js/browser/MainContent.js @@ -21,9 +21,11 @@ import ObjectsSection from "../objects/ObjectsSection" import MainActions from "./MainActions" import MakeBucketModal from "../buckets/MakeBucketModal" import UploadModal from "../uploads/UploadModal" +import ObjectsBulkActions from "../objects/ObjectsBulkActions" export const MainContent = () => (
+
diff --git a/browser/app/js/objects/ObjectContainer.js b/browser/app/js/objects/ObjectContainer.js index 4fdf938c5..237451a91 100644 --- a/browser/app/js/objects/ObjectContainer.js +++ b/browser/app/js/objects/ObjectContainer.js @@ -15,22 +15,41 @@ */ import React from "react" +import { connect } from "react-redux" import humanize from "humanize" import Moment from "moment" import ObjectItem from "./ObjectItem" import ObjectActions from "./ObjectActions" import * as actionsObjects from "./actions" +import { getCheckedList } from "./selectors" -export const ObjectContainer = ({ object }) => { - const actionButtons = - const props = { +export const ObjectContainer = ({ + object, + checkedObjectsCount, + downloadObject +}) => { + let props = { name: object.name, contentType: object.contentType, size: humanize.filesize(object.size), - lastModified: Moment(object.lastModified).format("lll"), - actionButtons: actionButtons + lastModified: Moment(object.lastModified).format("lll") } - return + if (checkedObjectsCount == 0) { + props.actionButtons = + } + return downloadObject(object.name)} /> } -export default ObjectContainer +const mapStateToProps = state => { + return { + checkedObjectsCount: getCheckedList(state).length + } +} + +const mapDispatchToProps = dispatch => { + return { + downloadObject: object => dispatch(actionsObjects.downloadObject(object)) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(ObjectContainer) diff --git a/browser/app/js/objects/ObjectItem.js b/browser/app/js/objects/ObjectItem.js index edce808c5..5994b0b37 100644 --- a/browser/app/js/objects/ObjectItem.js +++ b/browser/app/js/objects/ObjectItem.js @@ -19,12 +19,17 @@ import { connect } from "react-redux" import humanize from "humanize" import Moment from "moment" import { getDataType } from "../mime" +import * as actions from "./actions" +import { getCheckedList } from "./selectors" export const ObjectItem = ({ name, contentType, size, lastModified, + checked, + checkObject, + uncheckObject, actionButtons, onClick }) => { @@ -32,7 +37,14 @@ export const ObjectItem = ({
- + { + checked ? uncheckObject(name) : checkObject(name) + }} + />
@@ -55,4 +67,17 @@ export const ObjectItem = ({ ) } -export default ObjectItem +const mapStateToProps = (state, ownProps) => { + return { + checked: getCheckedList(state).indexOf(ownProps.name) >= 0 + } +} + +const mapDispatchToProps = dispatch => { + return { + checkObject: name => dispatch(actions.checkObject(name)), + uncheckObject: name => dispatch(actions.uncheckObject(name)) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(ObjectItem) diff --git a/browser/app/js/objects/ObjectsBulkActions.js b/browser/app/js/objects/ObjectsBulkActions.js new file mode 100644 index 000000000..fc63fc5b9 --- /dev/null +++ b/browser/app/js/objects/ObjectsBulkActions.js @@ -0,0 +1,101 @@ +/* + * 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 { connect } from "react-redux" +import classNames from "classnames" +import * as actions from "./actions" +import { getCheckedList } from "./selectors" +import DeleteObjectConfirmModal from "./DeleteObjectConfirmModal" + +export class ObjectsBulkActions extends React.Component { + constructor(props) { + super(props) + this.state = { + showDeleteConfirmation: false + } + } + deleteChecked() { + const { deleteChecked } = this.props + deleteChecked() + this.hideDeleteConfirmModal() + } + hideDeleteConfirmModal() { + this.setState({ + showDeleteConfirmation: false + }) + } + render() { + const { checkedObjectsCount, downloadChecked, clearChecked } = this.props + return ( +
0 + }) + } + > + + {checkedObjectsCount} Objects + selected + + + + + + + + + {this.state.showDeleteConfirmation && ( + + )} +
+ ) + } +} + +const mapStateToProps = state => { + return { + checkedObjectsCount: getCheckedList(state).length + } +} + +const mapDispatchToProps = dispatch => { + return { + downloadChecked: () => dispatch(actions.downloadCheckedObjects()), + clearChecked: () => dispatch(actions.resetCheckedList()), + deleteChecked: () => dispatch(actions.deleteCheckedObjects()) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(ObjectsBulkActions) diff --git a/browser/app/js/objects/__tests__/ObjectContainer.test.js b/browser/app/js/objects/__tests__/ObjectContainer.test.js index 67d7cc074..edffdf09b 100644 --- a/browser/app/js/objects/__tests__/ObjectContainer.test.js +++ b/browser/app/js/objects/__tests__/ObjectContainer.test.js @@ -25,7 +25,25 @@ describe("ObjectContainer", () => { it("should render ObjectItem with props", () => { const wrapper = shallow() - expect(wrapper.find("ObjectItem").length).toBe(1) - expect(wrapper.find("ObjectItem").prop("name")).toBe("test1.jpg") + expect(wrapper.find("Connect(ObjectItem)").length).toBe(1) + expect(wrapper.find("Connect(ObjectItem)").prop("name")).toBe("test1.jpg") + }) + + 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__/ObjectItem.test.js b/browser/app/js/objects/__tests__/ObjectItem.test.js index 507d52aa3..5a692b254 100644 --- a/browser/app/js/objects/__tests__/ObjectItem.test.js +++ b/browser/app/js/objects/__tests__/ObjectItem.test.js @@ -34,4 +34,27 @@ describe("ObjectItem", () => { wrapper.find("a").simulate("click", { preventDefault: jest.fn() }) expect(onClick).toHaveBeenCalled() }) + + it("should call checkObject when the object/prefix is checked", () => { + const checkObject = jest.fn() + const wrapper = shallow( + + ) + wrapper.find("input[type='checkbox']").simulate("change") + expect(checkObject).toHaveBeenCalledWith("test") + }) + + it("should render checked checkbox", () => { + const wrapper = shallow() + expect(wrapper.find("input[type='checkbox']").prop("checked")).toBeTruthy() + }) + + it("should call uncheckObject when the object/prefix is unchecked", () => { + const uncheckObject = jest.fn() + const wrapper = shallow( + + ) + wrapper.find("input[type='checkbox']").simulate("change") + expect(uncheckObject).toHaveBeenCalledWith("test") + }) }) diff --git a/browser/app/js/objects/__tests__/ObjectsBulkActions.test.js b/browser/app/js/objects/__tests__/ObjectsBulkActions.test.js new file mode 100644 index 000000000..9948bc1f2 --- /dev/null +++ b/browser/app/js/objects/__tests__/ObjectsBulkActions.test.js @@ -0,0 +1,74 @@ +/* + * 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 { ObjectsBulkActions } from "../ObjectsBulkActions" + +describe("ObjectsBulkActions", () => { + it("should render without crashing", () => { + shallow() + }) + + it("should show actions when checkObjectsCount is more than 0", () => { + const wrapper = shallow() + expect(wrapper.hasClass("list-actions-toggled")).toBeTruthy() + }) + + it("should call downloadChecked when download button is clicked", () => { + const downloadChecked = jest.fn() + const wrapper = shallow( + + ) + wrapper.find("#download-checked").simulate("click") + expect(downloadChecked).toHaveBeenCalled() + }) + + it("should call clearChecked when close button is clicked", () => { + const clearChecked = jest.fn() + const wrapper = shallow( + + ) + wrapper.find("#close-bulk-actions").simulate("click") + expect(clearChecked).toHaveBeenCalled() + }) + + it("shoud show DeleteObjectConfirmModal when delete-checked button is clicked", () => { + const wrapper = shallow() + wrapper.find("#delete-checked").simulate("click") + wrapper.update() + expect(wrapper.find("DeleteObjectConfirmModal").length).toBe(1) + }) + + it("shoud call deleteChecked when Delete is clicked on confirmation modal", () => { + const deleteChecked = jest.fn() + const wrapper = shallow( + + ) + wrapper.find("#delete-checked").simulate("click") + wrapper.update() + wrapper.find("DeleteObjectConfirmModal").prop("deleteObject")() + expect(deleteChecked).toHaveBeenCalled() + wrapper.update() + expect(wrapper.find("DeleteObjectConfirmModal").length).toBe(0) + }) +}) diff --git a/browser/app/js/objects/__tests__/ObjectsList.test.js b/browser/app/js/objects/__tests__/ObjectsList.test.js index 5d6609271..72bb3eed1 100644 --- a/browser/app/js/objects/__tests__/ObjectsList.test.js +++ b/browser/app/js/objects/__tests__/ObjectsList.test.js @@ -27,7 +27,7 @@ describe("ObjectsList", () => { const wrapper = shallow( ) - expect(wrapper.find("ObjectContainer").length).toBe(2) + expect(wrapper.find("Connect(ObjectContainer)").length).toBe(2) }) it("should render PrefixContainer for every prefix", () => { diff --git a/browser/app/js/objects/__tests__/PrefixContainer.test.js b/browser/app/js/objects/__tests__/PrefixContainer.test.js index 87bc47ba3..7ae27a0bb 100644 --- a/browser/app/js/objects/__tests__/PrefixContainer.test.js +++ b/browser/app/js/objects/__tests__/PrefixContainer.test.js @@ -25,8 +25,8 @@ describe("PrefixContainer", () => { it("should render ObjectItem with props", () => { const wrapper = shallow() - expect(wrapper.find("ObjectItem").length).toBe(1) - expect(wrapper.find("ObjectItem").prop("name")).toBe("abc/") + expect(wrapper.find("Connect(ObjectItem)").length).toBe(1) + expect(wrapper.find("Connect(ObjectItem)").prop("name")).toBe("abc/") }) it("should call selectPrefix when the prefix is clicked", () => { @@ -38,7 +38,7 @@ describe("PrefixContainer", () => { selectPrefix={selectPrefix} /> ) - wrapper.find("ObjectItem").prop("onClick")() + wrapper.find("Connect(ObjectItem)").prop("onClick")() expect(selectPrefix).toHaveBeenCalledWith("xyz/abc/") }) }) diff --git a/browser/app/js/objects/__tests__/actions.test.js b/browser/app/js/objects/__tests__/actions.test.js index 26d33a37c..7fa256895 100644 --- a/browser/app/js/objects/__tests__/actions.test.js +++ b/browser/app/js/objects/__tests__/actions.test.js @@ -18,8 +18,10 @@ import configureStore from "redux-mock-store" import thunk from "redux-thunk" import * as actionsObjects from "../actions" import * as alertActions from "../../alert/actions" +import { minioBrowserPrefix } from "../../constants" jest.mock("../../web", () => ({ + LoggedIn: jest.fn(() => true).mockReturnValueOnce(false), ListObjects: jest.fn(() => { return Promise.resolve({ objects: [{ name: "test1" }, { name: "test2" }], @@ -38,7 +40,18 @@ jest.mock("../../web", () => ({ return Promise.reject({ message: "Invalid bucket" }) } return Promise.resolve({ url: "https://test.com/bk1/pre1/b.txt" }) - }) + }), + CreateURLToken: jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve({ token: "test" }) + }) + .mockImplementationOnce(() => { + return Promise.reject({ message: "Error in creating token" }) + }) + .mockImplementationOnce(() => { + return Promise.resolve({ token: "test" }) + }) })) const middlewares = [thunk] @@ -169,13 +182,14 @@ describe("Objects actions", () => { expect(actions).toEqual(expectedActions) }) - it("should update browser url and creates objects/SET_CURRENT_PREFIX action when selectPrefix is called", () => { + it("should update browser url and creates objects/SET_CURRENT_PREFIX and objects/CHECKED_LIST_RESET actions when selectPrefix is called", () => { const store = mockStore({ buckets: { currentBucket: "test" }, objects: { currentPrefix: "" } }) const expectedActions = [ - { type: "objects/SET_CURRENT_PREFIX", prefix: "abc/" } + { type: "objects/SET_CURRENT_PREFIX", prefix: "abc/" }, + { type: "objects/CHECKED_LIST_RESET" } ] store.dispatch(actionsObjects.selectPrefix("abc/")) const actions = store.getActions() @@ -301,4 +315,132 @@ describe("Objects actions", () => { expect(actions).toEqual(expectedActions) }) }) + + describe("Download object", () => { + it("should download the object non-LoggedIn users", () => { + const setLocation = jest.fn() + Object.defineProperty(window, "location", { + set(url) { + setLocation(url) + } + }) + const store = mockStore({ + buckets: { currentBucket: "bk1" }, + objects: { currentPrefix: "pre1/" } + }) + store.dispatch(actionsObjects.downloadObject("obj1")) + const url = `${ + window.location.origin + }${minioBrowserPrefix}/download/bk1/${encodeURI("pre1/obj1")}?token=''` + expect(setLocation).toHaveBeenCalledWith(url) + }) + + it("should download the object for LoggedIn users", () => { + const setLocation = jest.fn() + Object.defineProperty(window, "location", { + set(url) { + setLocation(url) + } + }) + const store = mockStore({ + buckets: { currentBucket: "bk1" }, + objects: { currentPrefix: "pre1/" } + }) + return store.dispatch(actionsObjects.downloadObject("obj1")).then(() => { + const url = `${ + window.location.origin + }${minioBrowserPrefix}/download/bk1/${encodeURI( + "pre1/obj1" + )}?token=test` + expect(setLocation).toHaveBeenCalledWith(url) + }) + }) + + it("create alert/SET action when CreateUrlToken fails", () => { + const store = mockStore({ + buckets: { currentBucket: "bk1" }, + objects: { currentPrefix: "pre1/" } + }) + const expectedActions = [ + { + type: "alert/SET", + alert: { + type: "danger", + message: "Error in creating token", + id: alertActions.alertId + } + } + ] + return store.dispatch(actionsObjects.downloadObject("obj1")).then(() => { + const actions = store.getActions() + expect(actions).toEqual(expectedActions) + }) + }) + }) + + it("creates objects/CHECKED_LIST_ADD action", () => { + const store = mockStore() + const expectedActions = [ + { + type: "objects/CHECKED_LIST_ADD", + object: "obj1" + } + ] + store.dispatch(actionsObjects.checkObject("obj1")) + const actions = store.getActions() + expect(actions).toEqual(expectedActions) + }) + + it("creates objects/CHECKED_LIST_REMOVE action", () => { + const store = mockStore() + const expectedActions = [ + { + type: "objects/CHECKED_LIST_REMOVE", + object: "obj1" + } + ] + store.dispatch(actionsObjects.uncheckObject("obj1")) + const actions = store.getActions() + expect(actions).toEqual(expectedActions) + }) + + it("creates objects/CHECKED_LIST_RESET action", () => { + const store = mockStore() + const expectedActions = [ + { + type: "objects/CHECKED_LIST_RESET" + } + ] + store.dispatch(actionsObjects.resetCheckedList()) + const actions = store.getActions() + expect(actions).toEqual(expectedActions) + }) + + it("should download checked objects", () => { + 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/", checkedList: ["obj1"] } + }) + return store.dispatch(actionsObjects.downloadCheckedObjects()).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: ["obj1"] + }) + ) + }) + }) }) diff --git a/browser/app/js/objects/__tests__/reducer.test.js b/browser/app/js/objects/__tests__/reducer.test.js index 732b227ba..d3ea3ba07 100644 --- a/browser/app/js/objects/__tests__/reducer.test.js +++ b/browser/app/js/objects/__tests__/reducer.test.js @@ -31,7 +31,8 @@ describe("objects reducer", () => { show: false, object: "", url: "" - } + }, + checkedList: [] }) }) @@ -135,4 +136,33 @@ describe("objects reducer", () => { url: "test" }) }) + + it("should handle CHECKED_LIST_ADD", () => { + const newState = reducer(undefined, { + type: actions.CHECKED_LIST_ADD, + object: "obj1" + }) + expect(newState.checkedList).toEqual(["obj1"]) + }) + + it("should handle SELECTED_LIST_REMOVE", () => { + const newState = reducer( + { checkedList: ["obj1", "obj2"] }, + { + type: actions.CHECKED_LIST_REMOVE, + object: "obj1" + } + ) + expect(newState.checkedList).toEqual(["obj2"]) + }) + + it("should handle CHECKED_LIST_RESET", () => { + const newState = reducer( + { checkedList: ["obj1", "obj2"] }, + { + type: actions.CHECKED_LIST_RESET + } + ) + expect(newState.checkedList).toEqual([]) + }) }) diff --git a/browser/app/js/objects/actions.js b/browser/app/js/objects/actions.js index 3ae6fe606..788a4c91d 100644 --- a/browser/app/js/objects/actions.js +++ b/browser/app/js/objects/actions.js @@ -22,8 +22,9 @@ import { sortObjectsByDate } from "../utils" import { getCurrentBucket } from "../buckets/selectors" -import { getCurrentPrefix } from "./selectors" +import { getCurrentPrefix, getCheckedList } from "./selectors" import * as alertActions from "../alert/actions" +import { minioBrowserPrefix } from "../constants" export const SET_LIST = "objects/SET_LIST" export const APPEND_LIST = "objects/APPEND_LIST" @@ -32,6 +33,9 @@ export const SET_SORT_BY = "objects/SET_SORT_BY" export const SET_SORT_ORDER = "objects/SET_SORT_ORDER" export const SET_CURRENT_PREFIX = "objects/SET_CURRENT_PREFIX" export const SET_SHARE_OBJECT = "objects/SET_SHARE_OBJECT" +export const CHECKED_LIST_ADD = "objects/CHECKED_LIST_ADD" +export const CHECKED_LIST_REMOVE = "objects/CHECKED_LIST_REMOVE" +export const CHECKED_LIST_RESET = "objects/CHECKED_LIST_RESET" export const setList = (objects, marker, isTruncated) => ({ type: SET_LIST, @@ -119,6 +123,7 @@ export const selectPrefix = prefix => { return function(dispatch, getState) { dispatch(setCurrentPrefix(prefix)) dispatch(fetchObjects()) + dispatch(resetCheckedList()) const currentBucket = getCurrentBucket(getState()) history.replace(`/${currentBucket}/${prefix}`) } @@ -160,6 +165,16 @@ export const removeObject = object => ({ object }) +export const deleteCheckedObjects = () => { + return function(dispatch, getState) { + const checkedObjects = getCheckedList(getState()) + for (let i = 0; i < checkedObjects.length; i++) { + dispatch(deleteObject(checkedObjects[i])) + } + dispatch(resetCheckedList()) + } +} + export const shareObject = (object, days, hours, minutes) => { return function(dispatch, getState) { const currentBucket = getCurrentBucket(getState()) @@ -206,3 +221,112 @@ export const hideShareObject = (object, url) => ({ object: "", url: "" }) + +export const downloadObject = object => { + return function(dispatch, getState) { + const currentBucket = getCurrentBucket(getState()) + const currentPrefix = getCurrentPrefix(getState()) + const objectName = `${currentPrefix}${object}` + const encObjectName = encodeURI(objectName) + if (web.LoggedIn()) { + return web + .CreateURLToken() + .then(res => { + const url = `${ + window.location.origin + }${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=${ + res.token + }` + window.location = url + }) + .catch(err => { + dispatch( + alertActions.set({ + type: "danger", + message: err.message + }) + ) + }) + } else { + const url = `${ + window.location.origin + }${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=''` + window.location = url + } + } +} + +export const checkObject = object => ({ + type: CHECKED_LIST_ADD, + object +}) + +export const uncheckObject = object => ({ + type: CHECKED_LIST_REMOVE, + object +}) + +export const resetCheckedList = () => ({ + type: CHECKED_LIST_RESET +}) + +export const downloadCheckedObjects = () => { + return function(dispatch, getState) { + const state = getState() + const req = { + bucketName: getCurrentBucket(state), + prefix: getCurrentPrefix(state), + objects: getCheckedList(state) + } + if (!web.LoggedIn()) { + const requestUrl = location.origin + "/minio/zip?token=''" + downloadZip(requestUrl, req, dispatch) + } else { + return web + .CreateURLToken() + .then(res => { + const requestUrl = `${ + location.origin + }${minioBrowserPrefix}/zip?token=${res.token}` + downloadZip(requestUrl, req, dispatch) + }) + .catch(err => + dispatch( + alertActions.set({ + type: "danger", + message: err.message + }) + ) + ) + } + } +} + +const downloadZip = (url, req, dispatch) => { + var anchor = document.createElement("a") + document.body.appendChild(anchor) + + var xhr = new XMLHttpRequest() + xhr.open("POST", url, true) + xhr.responseType = "blob" + + xhr.onload = function(e) { + if (this.status == 200) { + dispatch(resetCheckedList()) + var blob = new Blob([this.response], { + type: "octet/stream" + }) + var blobUrl = window.URL.createObjectURL(blob) + var separator = req.prefix.length > 1 ? "-" : "" + + anchor.href = blobUrl + anchor.download = + req.bucketName + separator + req.prefix.slice(0, -1) + ".zip" + + anchor.click() + window.URL.revokeObjectURL(blobUrl) + anchor.remove() + } + } + xhr.send(JSON.stringify(req)) +} diff --git a/browser/app/js/objects/reducer.js b/browser/app/js/objects/reducer.js index 301b9aa83..5d5941ebd 100644 --- a/browser/app/js/objects/reducer.js +++ b/browser/app/js/objects/reducer.js @@ -16,8 +16,8 @@ import * as actionsObjects from "./actions" -const removeObject = (list, action) => { - const idx = list.findIndex(object => object.name === action.object) +const removeObject = (list, objectToRemove, lookup) => { + const idx = list.findIndex(object => lookup(object) === objectToRemove) if (idx == -1) { return list } @@ -36,7 +36,8 @@ export default ( show: false, object: "", url: "" - } + }, + checkedList: [] }, action ) => { @@ -58,7 +59,7 @@ export default ( case actionsObjects.REMOVE: return { ...state, - list: removeObject(state.list, action) + list: removeObject(state.list, action.object, object => object.name) } case actionsObjects.SET_SORT_BY: return { @@ -86,6 +87,25 @@ export default ( url: action.url } } + case actionsObjects.CHECKED_LIST_ADD: + return { + ...state, + checkedList: [...state.checkedList, action.object] + } + case actionsObjects.CHECKED_LIST_REMOVE: + return { + ...state, + checkedList: removeObject( + state.checkedList, + action.object, + object => object + ) + } + case actionsObjects.CHECKED_LIST_RESET: + return { + ...state, + checkedList: [] + } default: return state } diff --git a/browser/app/js/objects/selectors.js b/browser/app/js/objects/selectors.js index 45809c582..1e9895a51 100644 --- a/browser/app/js/objects/selectors.js +++ b/browser/app/js/objects/selectors.js @@ -17,3 +17,5 @@ import { createSelector } from "reselect" export const getCurrentPrefix = state => state.objects.currentPrefix + +export const getCheckedList = state => state.objects.checkedList