diff --git a/frontend/packages/kitconcept-core/news/+dragDropFolderContent.feature b/frontend/packages/kitconcept-core/news/+dragDropFolderContent.feature new file mode 100644 index 00000000..ca107bb3 --- /dev/null +++ b/frontend/packages/kitconcept-core/news/+dragDropFolderContent.feature @@ -0,0 +1 @@ +Add feature of drag and drop files in folder contents @Tishasoumya-02 \ No newline at end of file diff --git a/frontend/packages/kitconcept-core/src/components/Contents/Contents.jsx b/frontend/packages/kitconcept-core/src/components/Contents/Contents.jsx new file mode 100644 index 00000000..c14c670c --- /dev/null +++ b/frontend/packages/kitconcept-core/src/components/Contents/Contents.jsx @@ -0,0 +1,2006 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { createPortal } from 'react-dom'; +import { Link } from 'react-router-dom'; +import { + Button, + Container as SemanticContainer, + Divider, + Dropdown, + Menu, + Input, + Segment, + Table, + Loader, + Dimmer, +} from 'semantic-ui-react'; +import concat from 'lodash/concat'; +import filter from 'lodash/filter'; +import find from 'lodash/find'; +import indexOf from 'lodash/indexOf'; +import keys from 'lodash/keys'; +import map from 'lodash/map'; +import mapValues from 'lodash/mapValues'; +import pull from 'lodash/pull'; +import move from 'lodash-move'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import { asyncConnect } from '@plone/volto/helpers/AsyncConnect'; +import { getBaseUrl } from '@plone/volto/helpers/Url/Url'; + +import { searchContent } from '@plone/volto/actions/search/search'; +import { + deleteContent, + orderContent, + sortContent, + updateColumnsContent, + getContent, +} from '@plone/volto/actions/content/content'; +import { + copyContent, + moveContent, + cut, + copy, +} from '@plone/volto/actions/clipboard/clipboard'; +import { listActions } from '@plone/volto/actions/actions/actions'; +import Indexes, { defaultIndexes } from '@plone/volto/constants/Indexes'; +import Pagination from '@plone/volto/components/theme/Pagination/Pagination'; +import Popup from '@plone/volto/components/theme/Popup/Popup'; +import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar'; +import Toast from '@plone/volto/components/manage/Toast/Toast'; +import Icon from '@plone/volto/components/theme/Icon/Icon'; +import Unauthorized from '@plone/volto/components/theme/Unauthorized/Unauthorized'; +import ContentsBreadcrumbs from '@plone/volto/components/manage/Contents/ContentsBreadcrumbs'; +import ContentsIndexHeader from '@plone/volto/components/manage/Contents/ContentsIndexHeader'; +import ContentsItem from '@plone/volto/components/manage/Contents/ContentsItem'; +import { ContentsRenameModal } from '@plone/volto/components/manage/Contents'; +import ContentsUploadModal from '@plone/volto/components/manage/Contents/ContentsUploadModal'; +import ContentsDeleteModal from '@plone/volto/components/manage/Contents/ContentsDeleteModal'; +import ContentsWorkflowModal from '@plone/volto/components/manage/Contents/ContentsWorkflowModal'; +import ContentsTagsModal from '@plone/volto/components/manage/Contents/ContentsTagsModal'; +import ContentsPropertiesModal from '@plone/volto/components/manage/Contents/ContentsPropertiesModal'; + +import Helmet from '@plone/volto/helpers/Helmet/Helmet'; +import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; +import config from '@plone/volto/registry'; + +import backSVG from '@plone/volto/icons/back.svg'; +import cutSVG from '@plone/volto/icons/cut.svg'; +import deleteSVG from '@plone/volto/icons/delete.svg'; +import copySVG from '@plone/volto/icons/copy.svg'; +import tagSVG from '@plone/volto/icons/tag.svg'; +import renameSVG from '@plone/volto/icons/rename.svg'; +import semaphoreSVG from '@plone/volto/icons/semaphore.svg'; +import uploadSVG from '@plone/volto/icons/upload.svg'; +import propertiesSVG from '@plone/volto/icons/properties.svg'; +import pasteSVG from '@plone/volto/icons/paste.svg'; +import zoomSVG from '@plone/volto/icons/zoom.svg'; +import checkboxUncheckedSVG from '@plone/volto/icons/checkbox-unchecked.svg'; +import checkboxCheckedSVG from '@plone/volto/icons/checkbox-checked.svg'; +import checkboxIndeterminateSVG from '@plone/volto/icons/checkbox-indeterminate.svg'; +import configurationSVG from '@plone/volto/icons/configuration-app.svg'; +import sortDownSVG from '@plone/volto/icons/sort-down.svg'; +import sortUpSVG from '@plone/volto/icons/sort-up.svg'; +import downKeySVG from '@plone/volto/icons/down-key.svg'; +import moreSVG from '@plone/volto/icons/more.svg'; +import clearSVG from '@plone/volto/icons/clear.svg'; +import DropzoneContent from './DropZoneContent'; + +const messages = defineMessages({ + back: { + id: 'Back', + defaultMessage: 'Back', + }, + contents: { + id: 'Contents', + defaultMessage: 'Contents', + }, + copy: { + id: 'Copy', + defaultMessage: 'Copy', + }, + cut: { + id: 'Cut', + defaultMessage: 'Cut', + }, + error: { + id: "You can't paste this content here", + defaultMessage: "You can't paste this content here", + }, + delete: { + id: 'Delete', + defaultMessage: 'Delete', + }, + deleteError: { + id: 'The item could not be deleted.', + defaultMessage: 'The item could not be deleted.', + }, + loading: { + id: 'loading', + defaultMessage: 'Loading', + }, + home: { + id: 'Home', + defaultMessage: 'Home', + }, + filter: { + id: 'Filter…', + defaultMessage: 'Filter…', + }, + messageCopied: { + id: 'Item(s) copied.', + defaultMessage: 'Item(s) copied.', + }, + messageCut: { + id: 'Item(s) cut.', + defaultMessage: 'Item(s) cut.', + }, + messageUpdate: { + id: 'Item(s) has been updated.', + defaultMessage: 'Item(s) has been updated.', + }, + messageReorder: { + id: 'Item successfully moved.', + defaultMessage: 'Item successfully moved.', + }, + messagePasted: { + id: 'Item(s) pasted.', + defaultMessage: 'Item(s) pasted.', + }, + messageWorkflowUpdate: { + id: 'Item(s) state has been updated.', + defaultMessage: 'Item(s) state has been updated.', + }, + paste: { + id: 'Paste', + defaultMessage: 'Paste', + }, + properties: { + id: 'Properties', + defaultMessage: 'Properties', + }, + rearrangeBy: { + id: 'Rearrange items by…', + defaultMessage: 'Rearrange items by…', + }, + rename: { + id: 'Rename', + defaultMessage: 'Rename', + }, + select: { + id: 'Select…', + defaultMessage: 'Select…', + }, + selected: { + id: '{count} selected', + defaultMessage: '{count} selected', + }, + selectColumns: { + id: 'Select columns to show', + defaultMessage: 'Select columns to show', + }, + sort: { + id: 'sort', + defaultMessage: 'sort', + }, + state: { + id: 'State', + defaultMessage: 'State', + }, + tags: { + id: 'Tags', + defaultMessage: 'Tags', + }, + upload: { + id: 'Upload', + defaultMessage: 'Upload', + }, + success: { + id: 'Success', + defaultMessage: 'Success', + }, + publicationDate: { + id: 'Publication date', + defaultMessage: 'Publication date', + }, + createdOn: { + id: 'Created on', + defaultMessage: 'Created on', + }, + expirationDate: { + id: 'Expiration date', + defaultMessage: 'Expiration date', + }, + id: { + id: 'ID', + defaultMessage: 'ID', + }, + uid: { + id: 'UID', + defaultMessage: 'UID', + }, + reviewState: { + id: 'Review state', + defaultMessage: 'Review state', + }, + folder: { + id: 'Folder', + defaultMessage: 'Folder', + }, + excludedFromNavigation: { + id: 'Excluded from navigation', + defaultMessage: 'Excluded from navigation', + }, + objectSize: { + id: 'Object Size', + defaultMessage: 'Object Size', + }, + lastCommentedDate: { + id: 'Last comment date', + defaultMessage: 'Last comment date', + }, + totalComments: { + id: 'Total comments', + defaultMessage: 'Total comments', + }, + creator: { + id: 'Creator', + defaultMessage: 'Creator', + }, + endDate: { + id: 'End Date', + defaultMessage: 'End Date', + }, + startDate: { + id: 'Start Date', + defaultMessage: 'Start Date', + }, + all: { + id: 'All', + defaultMessage: 'All', + }, + resultCount: { + id: 'resultCount', + defaultMessage: 'Result count', + }, +}); + +/** + * Contents class. + * @class Contents + * @extends Component + */ +class Contents extends Component { + /** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ + static propTypes = { + action: PropTypes.string, + source: PropTypes.arrayOf(PropTypes.string), + searchContent: PropTypes.func.isRequired, + cut: PropTypes.func.isRequired, + copy: PropTypes.func.isRequired, + copyContent: PropTypes.func.isRequired, + deleteContent: PropTypes.func.isRequired, + moveContent: PropTypes.func.isRequired, + orderContent: PropTypes.func.isRequired, + sortContent: PropTypes.func.isRequired, + updateColumnsContent: PropTypes.func.isRequired, + clipboardRequest: PropTypes.shape({ + loading: PropTypes.bool, + loaded: PropTypes.bool, + }).isRequired, + deleteRequest: PropTypes.shape({ + loading: PropTypes.bool, + loaded: PropTypes.bool, + }).isRequired, + updateRequest: PropTypes.shape({ + loading: PropTypes.bool, + loaded: PropTypes.bool, + }).isRequired, + searchRequest: PropTypes.shape({ + loading: PropTypes.bool, + loaded: PropTypes.bool, + }).isRequired, + items: PropTypes.arrayOf( + PropTypes.shape({ + '@id': PropTypes.string, + '@type': PropTypes.string, + title: PropTypes.string, + description: PropTypes.string, + }), + ), + breadcrumbs: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string, + url: PropTypes.string, + }), + ).isRequired, + total: PropTypes.number.isRequired, + pathname: PropTypes.string.isRequired, + }; + + /** + * Default properties. + * @property {Object} defaultProps Default properties. + * @static + */ + static defaultProps = { + items: [], + action: null, + source: null, + index: { + order: keys(Indexes), + values: mapValues(Indexes, (value, key) => ({ + ...value, + selected: indexOf(defaultIndexes, key) !== -1, + })), + selectedCount: defaultIndexes.length + 1, + }, + }; + + /** + * Constructor + * @method constructor + * @param {Object} props Component properties + * @constructs ContentsComponent + */ + constructor(props) { + super(props); + this.onDeselect = this.onDeselect.bind(this); + this.onSelect = this.onSelect.bind(this); + this.onSelectAll = this.onSelectAll.bind(this); + this.onSelectIndex = this.onSelectIndex.bind(this); + this.onSelectNone = this.onSelectNone.bind(this); + this.onDeleteOk = this.onDeleteOk.bind(this); + this.onDeleteCancel = this.onDeleteCancel.bind(this); + this.onUploadOk = this.onUploadOk.bind(this); + this.onUploadCancel = this.onUploadCancel.bind(this); + this.onRenameOk = this.onRenameOk.bind(this); + this.onRenameCancel = this.onRenameCancel.bind(this); + this.onTagsOk = this.onTagsOk.bind(this); + this.onTagsCancel = this.onTagsCancel.bind(this); + this.onPropertiesOk = this.onPropertiesOk.bind(this); + this.onPropertiesCancel = this.onPropertiesCancel.bind(this); + this.onWorkflowOk = this.onWorkflowOk.bind(this); + this.onWorkflowCancel = this.onWorkflowCancel.bind(this); + this.onChangeFilter = this.onChangeFilter.bind(this); + this.onChangePage = this.onChangePage.bind(this); + this.onChangePageSize = this.onChangePageSize.bind(this); + this.onOrderIndex = this.onOrderIndex.bind(this); + this.onOrderItem = this.onOrderItem.bind(this); + this.onSortItems = this.onSortItems.bind(this); + this.onMoveToTop = this.onMoveToTop.bind(this); + this.onChangeSelected = this.onChangeSelected.bind(this); + this.onMoveToBottom = this.onMoveToBottom.bind(this); + this.cut = this.cut.bind(this); + this.copy = this.copy.bind(this); + this.delete = this.delete.bind(this); + this.upload = this.upload.bind(this); + this.rename = this.rename.bind(this); + this.tags = this.tags.bind(this); + this.properties = this.properties.bind(this); + this.workflow = this.workflow.bind(this); + this.paste = this.paste.bind(this); + this.fetchContents = this.fetchContents.bind(this); + this.orderTimeout = null; + + this.state = { + selected: [], + showDelete: false, + showUpload: false, + showRename: false, + showTags: false, + showProperties: false, + showWorkflow: false, + itemsToDelete: [], + items: this.props.items, + filter: '', + currentPage: 0, + pageSize: 50, + index: this.props.index || { + order: keys(Indexes), + values: mapValues(Indexes, (value, key) => ({ + ...value, + selected: indexOf(defaultIndexes, key) !== -1, + })), + selectedCount: defaultIndexes.length + 1, + }, + sort_on: this.props.sort?.on || 'getObjPositionInParent', + sort_order: this.props.sort?.order || 'ascending', + isClient: false, + }; + this.filterTimeout = null; + } + + /** + * Component did mount + * @method componentDidMount + * @returns {undefined} + */ + componentDidMount() { + this.fetchContents(); + this.setState({ isClient: true }); + } + + /** + * Component will receive props + * @method componentWillReceiveProps + * @param {Object} nextProps Next properties + * @returns {undefined} + */ + UNSAFE_componentWillReceiveProps(nextProps) { + if ( + (this.props.clipboardRequest.loading && + nextProps.clipboardRequest.loaded) || + (this.props.deleteRequest.loading && nextProps.deleteRequest.loaded) || + (this.props.updateRequest.loading && nextProps.updateRequest.loaded) + ) { + this.fetchContents(nextProps.pathname); + } + if (this.props.updateRequest.loading && nextProps.updateRequest.loaded) { + this.props.toastify.toast.success( + , + ); + } + if (this.props.pathname !== nextProps.pathname) { + // Refetching content to sync the current object in the toolbar + this.props.getContent(getBaseUrl(nextProps.pathname)); + this.setState( + { + currentPage: 0, + selected: [], + }, + () => + this.setState({ filter: '' }, () => + this.fetchContents(nextProps.pathname), + ), + ); + } + if (this.props.searchRequest.loading && nextProps.searchRequest.loaded) { + this.setState({ + items: nextProps.items, + }); + } + if ( + this.props.clipboardRequest.loading && + nextProps.clipboardRequest.error + ) { + const msgBody = + nextProps.clipboardRequest.error?.response?.body?.message || + this.props.intl.formatMessage(messages.error); + this.props.toastify.toast.error( + , + ); + } + + if ( + this.props.clipboardRequest.loading && + nextProps.clipboardRequest.loaded + ) { + this.props.toastify.toast.success( + , + ); + } + + if (this.props.deleteRequest.loading && nextProps.deleteRequest.error) { + this.props.toastify.toast.error( + , + ); + } + + if (this.props.orderRequest.loading && nextProps.orderRequest.loaded) { + this.props.toastify.toast.success( + , + ); + } + } + + /** + * On deselect handler + * @method onDeselect + * @param {object} event Event object + * @param {string} value Value + * @returns {undefined} + */ + onDeselect(event, { value }) { + this.setState({ + selected: pull(this.state.selected, value), + }); + } + + /** + * On select handler + * @method onSelect + * @param {object} event Event object + * @returns {undefined} + */ + onSelect(event, id) { + if (indexOf(this.state.selected, id) === -1) { + this.setState({ + selected: concat(this.state.selected, id), + }); + } else { + this.setState({ + selected: pull(this.state.selected, id), + }); + } + } + + /** + * On select all handler + * @method onSelectAll + * @returns {undefined} + */ + onSelectAll() { + this.setState({ + selected: map(this.state.items, (item) => item['@id']), + }); + } + + /** + * On select none handler + * @method onSelectNone + * @returns {undefined} + */ + onSelectNone() { + this.setState({ + selected: [], + }); + } + + /** + * On select index + * @method onSelectIndex + * @param {object} event Event object. + * @param {string} value Index value. + * @returns {undefined} + */ + onSelectIndex(event, { value }) { + let newIndex = { + ...this.state.index, + selectedCount: + this.state.index.selectedCount + + (this.state.index.values[value].selected ? -1 : 1), + values: mapValues(this.state.index.values, (indexValue, indexKey) => ({ + ...indexValue, + selected: + indexKey === value ? !indexValue.selected : indexValue.selected, + })), + }; + this.setState({ + index: newIndex, + }); + this.props.updateColumnsContent(getBaseUrl(this.props.pathname), newIndex); + } + + /** + * On change filter + * @method onChangeFilter + * @param {object} event Event object. + * @param {string} value Filter value. + * @returns {undefined} + */ + onChangeFilter(event, { value }) { + const self = this; + clearTimeout(self.filterTimeout); + this.setState( + { + filter: value, + }, + () => { + self.filterTimeout = setTimeout(() => { + self.fetchContents(); + }, 200); + }, + ); + } + + /** + * On change selected values (Filter) + * @method onChangeSelected + * @param {object} event Event object. + * @param {string} value Filter value. + * @returns {undefined} + */ + onChangeSelected(event, { value }) { + event.stopPropagation(); + const { items, selected } = this.state; + + const filteredItems = filter(selected, (selectedItem) => + find(items, (item) => item['@id'] === selectedItem) + .title.toLowerCase() + .includes(value.toLowerCase()), + ); + + this.setState({ + filteredItems, + selectedMenuFilter: value, + }); + } + + /** + * On change page + * @method onChangePage + * @param {object} event Event object. + * @param {string} value Page value. + * @returns {undefined} + */ + onChangePage(event, { value }) { + this.setState( + { + currentPage: value, + selected: [], + }, + () => this.fetchContents(), + ); + } + + /** + * On change page size + * @method onChangePageSize + * @param {object} event Event object. + * @param {string} value Page size value. + * @returns {undefined} + */ + onChangePageSize(event, { value }) { + this.setState( + { + pageSize: value, + currentPage: 0, + selected: [], + }, + () => this.fetchContents(), + ); + } + + /** + * On order index + * @method onOrderIndex + * @param {number} index Index + * @param {number} delta Delta + * @returns {undefined} + */ + onOrderIndex(index, delta) { + this.setState({ + index: { + ...this.state.index, + order: move(this.state.index.order, index, index + delta), + }, + }); + this.props.updateColumnsContent( + getBaseUrl(this.props.pathname), + this.state.index, + ); + } + + /** + * On order item + * @method onOrderItem + * @param {string} id Item id + * @param {number} itemIndex Item index + * @param {number} delta Delta + * @returns {undefined} + */ + onOrderItem(id, itemIndex, delta, backend) { + if (backend) { + this.props.orderContent( + getBaseUrl(this.props.pathname), + id.replace(/^.*\//, ''), + delta, + ); + } else { + this.setState({ + items: move(this.state.items, itemIndex, itemIndex + delta), + }); + } + } + + /** + * On sort items + * @method onSortItems + * @param {object} event Event object + * @param {string} value Item index + * @returns {undefined} + */ + onSortItems(event, { value }) { + const values = value.split('|'); + this.setState({ + sort_on: values[0], + sort_order: values[1], + selected: [], + }); + this.props.sortContent( + getBaseUrl(this.props.pathname), + values[0], + values[1], + ); + } + + /** + * On move to top + * @method onMoveToTop + * @param {object} event Event object + * @param {string} value Item index + * @returns {undefined} + */ + onMoveToTop(event, { value }) { + const id = this.state.items[value]['@id']; + this.props + .orderContent( + getBaseUrl(this.props.pathname), + id.replace(/^.*\//, ''), + 'top', + ) + .then(() => { + this.setState( + { + currentPage: 0, + selected: [], + }, + () => this.fetchContents(), + ); + }); + } + + /** + * On move to bottom + * @method onMoveToBottom + * @param {object} event Event object + * @param {string} value Item index + * @returns {undefined} + */ + onMoveToBottom(event, { value }) { + const id = this.state.items[value]['@id']; + this.props + .orderContent( + getBaseUrl(this.props.pathname), + id.replace(/^.*\//, ''), + 'bottom', + ) + .then(() => { + this.setState( + { + currentPage: 0, + selected: [], + }, + () => this.fetchContents(), + ); + }); + } + + /** + * On delete ok + * @method onDeleteOk + * @returns {undefined} + */ + onDeleteOk() { + this.props.deleteContent(this.state.itemsToDelete); + this.setState({ + showDelete: false, + itemsToDelete: [], + selected: [], + }); + } + + /** + * On delete cancel + * @method onDeleteCancel + * @returns {undefined} + */ + onDeleteCancel() { + this.setState({ + showDelete: false, + itemsToDelete: [], + }); + } + + /** + * On upload ok + * @method onUploadOk + * @returns {undefined} + */ + onUploadOk() { + this.fetchContents(); + this.setState({ + showUpload: false, + }); + } + + /** + * On upload cancel + * @method onUploadCancel + * @returns {undefined} + */ + onUploadCancel() { + this.setState({ + showUpload: false, + }); + } + + /** + * On rename ok + * @method onRenameOk + * @returns {undefined} + */ + onRenameOk() { + this.setState({ + showRename: false, + selected: [], + }); + } + + /** + * On rename cancel + * @method onRenameCancel + * @returns {undefined} + */ + onRenameCancel() { + this.setState({ + showRename: false, + }); + } + + /** + * On tags ok + * @method onTagsOk + * @returns {undefined} + */ + onTagsOk() { + this.setState({ + showTags: false, + selected: [], + }); + } + + /** + * On tags cancel + * @method onTagsCancel + * @returns {undefined} + */ + onTagsCancel() { + this.setState({ + showTags: false, + }); + } + + /** + * On properties ok + * @method onPropertiesOk + * @returns {undefined} + */ + onPropertiesOk() { + this.setState({ + showProperties: false, + selected: [], + }); + } + + /** + * On properties cancel + * @method onPropertiesCancel + * @returns {undefined} + */ + onPropertiesCancel() { + this.setState({ + showProperties: false, + }); + } + + /** + * On workflow ok + * @method onWorkflowOk + * @returns {undefined} + */ + onWorkflowOk() { + this.fetchContents(); + this.setState({ + showWorkflow: false, + selected: [], + }); + this.props.toastify.toast.success( + , + ); + } + + /** + * On workflow cancel + * @method onWorkflowCancel + * @returns {undefined} + */ + onWorkflowCancel() { + this.setState({ + showWorkflow: false, + }); + } + + /** + * Get field by id + * @method getFieldById + * @param {string} id Id of object + * @param {string} field Field of object + * @returns {string} Field of object + */ + getFieldById(id, field) { + const item = find(this.state.items, { '@id': id }); + return item ? item[field] : ''; + } + + /** + * Fetch contents handler + * @method fetchContents + * @param {string} pathname Pathname to fetch contents. + * @returns {undefined} + */ + fetchContents(pathname) { + if (this.state.pageSize === this.props.intl.formatMessage(messages.all)) { + //'All' + this.props.searchContent(getBaseUrl(pathname || this.props.pathname), { + 'path.depth': 1, + sort_on: this.state.sort_on, + sort_order: this.state.sort_order, + metadata_fields: '_all', + b_size: 100000000, + show_inactive: true, + ...(this.state.filter && { SearchableText: `${this.state.filter}*` }), + }); + } else { + this.props.searchContent(getBaseUrl(pathname || this.props.pathname), { + 'path.depth': 1, + sort_on: this.state.sort_on, + sort_order: this.state.sort_order, + metadata_fields: '_all', + ...(this.state.filter && { SearchableText: `${this.state.filter}*` }), + b_size: this.state.pageSize, + b_start: this.state.currentPage * this.state.pageSize, + show_inactive: true, + }); + } + } + + /** + * Cut handler + * @method cut + * @param {Object} event Event object. + * @param {string} value Value of the event. + * @returns {undefined} + */ + cut(event, { value }) { + this.props.cut(value ? [value] : this.state.selected); + this.onSelectNone(); + this.props.toastify.toast.success( + , + ); + } + + /** + * Copy handler + * @method copy + * @param {Object} event Event object. + * @param {string} value Value of the event. + * @returns {undefined} + */ + copy(event, { value }) { + this.props.copy(value ? [value] : this.state.selected); + this.onSelectNone(); + this.props.toastify.toast.success( + , + ); + } + + /** + * Delete handler + * @method delete + * @param {Object} event Event object. + * @param {string} value Value of the event. + * @returns {undefined} + */ + delete(event, { value }) { + this.setState({ + showDelete: true, + itemsToDelete: value ? [value] : this.state.selected, + }); + } + + /** + * Upload handler + * @method upload + * @returns {undefined} + */ + upload() { + this.setState({ + showUpload: true, + }); + } + + /** + * Rename handler + * @method rename + * @returns {undefined} + */ + rename() { + this.setState({ + showRename: true, + }); + } + + /** + * Tags handler + * @method tags + * @returns {undefined} + */ + tags() { + this.setState({ + showTags: true, + }); + } + + /** + * Properties handler + * @method properties + * @returns {undefined} + */ + properties() { + this.setState({ + showProperties: true, + }); + } + + /** + * Workflow handler + * @method workflow + * @returns {undefined} + */ + workflow() { + this.setState({ + showWorkflow: true, + }); + } + + /** + * Paste handler + * @method paste + * @returns {undefined} + */ + paste() { + if (this.props.action === 'copy') { + this.props.copyContent( + this.props.source, + getBaseUrl(this.props.pathname), + ); + } + if (this.props.action === 'cut') { + this.props.moveContent( + this.props.source, + getBaseUrl(this.props.pathname), + ); + } + } + + /** + * Render method. + * @method render + * @returns {string} Markup for the component. + */ + render() { + const selected = this.state.selected.length > 0; + const filteredItems = this.state.filteredItems || this.state.selected; + const path = getBaseUrl(this.props.pathname); + const folderContentsAction = find(this.props.objectActions, { + id: 'folderContents', + }); + const loading = + (this.props.clipboardRequest?.loading && + !this.props.clipboardRequest?.error) || + (this.props.deleteRequest?.loading && !this.props.deleteRequest?.error) || + (this.props.updateRequest?.loading && !this.props.updateRequest?.error) || + (this.props.orderRequest?.loading && !this.props.orderRequest?.error) || + (this.props.searchRequest?.loading && !this.props.searchRequest?.error); + + const Container = + config.getComponent({ name: 'Container' }).component || SemanticContainer; + + return this.props.token && this.props.objectActions?.length > 0 ? ( + <> + {folderContentsAction ? ( + + + + + {this.props.intl.formatMessage(messages.loading)} + + + + {/* Start Customization */} + + {/* End Customization */} +
+
+ 1 + } + /> + + ({ + url: item, + title: this.getFieldById(item, 'title'), + id: this.getFieldById(item, 'id'), + }))} + /> + ({ + url: item, + subjects: this.getFieldById(item, 'Subject'), + }))} + /> + + find(this.state.items, { '@id': id }), + ).filter((item) => item)} + /> + {this.state.showWorkflow && ( + + )} +
+ + + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.upload, + )} + size="mini" + /> + + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.rename, + )} + size="mini" + /> + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.state, + )} + size="mini" + /> + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.tags, + )} + size="mini" + /> + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.properties, + )} + size="mini" + /> + + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.cut, + )} + size="mini" + /> + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.copy, + )} + size="mini" + /> + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.paste, + )} + size="mini" + /> + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.delete, + )} + size="mini" + /> + + +
+ + {this.state.filter && ( + + )} + +
+
+ +
+ + + + } + className="right floating selectIndex" + > + + + + {map( + filter( + this.state.index.order, + (index) => index !== 'sortable_title', + ), + (index) => ( + + {this.state.index.values[index] + .selected ? ( + + ) : ( + + )} + + {' '} + {this.props.intl.formatMessage({ + id: this.state.index.values[index] + .label, + defaultMessage: + this.state.index.values[index] + .label, + })} + + + ), + )} + + + + +
+ + {`${this.props.intl.formatMessage( + messages.resultCount, + )}: ${this.props.total || 0}`} + + + + + + + } + > + + + {map( + [ + 'id', + 'sortable_title', + 'EffectiveDate', + 'CreationDate', + 'ModificationDate', + 'portal_type', + ], + (index) => ( + + } + text={this.props.intl.formatMessage( + { + id: Indexes[index].label, + }, + )} + > + + + {' '} + + + + {' '} + + + + + ), + )} + + + + + 0 + ? '#007eb1' + : '#826a6a' + } + className="dropdown-popup-trigger" + size="24px" + /> + } + > + + + + {' '} + + + + {' '} + + + + + + } + iconPosition="left" + className="item search" + placeholder={this.props.intl.formatMessage( + messages.filter, + )} + value={ + this.state.selectedMenuFilter || '' + } + onChange={this.onChangeSelected} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + /> + + {map(filteredItems, (item) => ( + + {' '} + {this.getFieldById(item, 'title')} + + ))} + + + + + + + + {map( + this.state.index.order, + (index, order) => + this.state.index.values[index].selected && ( + + ), + )} + + + + + + + {this.state.items.map((item, order) => ( + ({ + id: index, + type: this.state.index.values[index].type, + })), + (index) => + this.state.index.values[index.id] + .selected, + )} + onCut={this.cut} + onCopy={this.copy} + onDelete={this.delete} + onOrderItem={this.onOrderItem} + onMoveToTop={this.onMoveToTop} + onMoveToBottom={this.onMoveToBottom} + /> + ))} + +
+
+ +
+ +
+
+
+
+
+
+ {this.state.isClient && + createPortal( + + + + } + />, + document.getElementById('toolbar'), + )} +
+
+ ) : ( + + )} + + ) : ( + + ); + } +} + +let dndContext; + +const DragDropConnector = (props) => { + const { DragDropContext } = props.reactDnd; + const HTML5Backend = props.reactDndHtml5Backend.default; + + const DndConnectedContents = React.useMemo(() => { + if (!dndContext) { + dndContext = DragDropContext(HTML5Backend); + } + return dndContext(Contents); + }, [DragDropContext, HTML5Backend]); + + return ; +}; + +export const __test__ = compose( + injectIntl, + injectLazyLibs(['toastify', 'reactDnd']), + connect( + (store, props) => { + return { + token: store.userSession.token, + items: store.search.items, + sort: store.content.update.sort, + index: store.content.updatecolumns.idx, + breadcrumbs: store.breadcrumbs.items, + total: store.search.total, + searchRequest: { + loading: store.search.loading, + loaded: store.search.loaded, + }, + pathname: props.location.pathname, + action: store.clipboard.action, + source: store.clipboard.source, + clipboardRequest: store.clipboard.request, + deleteRequest: store.content.delete, + updateRequest: store.content.update, + objectActions: store.actions.actions.object, + orderRequest: store.content.order, + }; + }, + { + searchContent, + cut, + copy, + copyContent, + deleteContent, + listActions, + moveContent, + orderContent, + sortContent, + updateColumnsContent, + getContent, + }, + ), +)(Contents); + +export default compose( + injectIntl, + connect( + (store, props) => { + return { + token: store.userSession.token, + items: store.search.items, + sort: store.content.update.sort, + index: store.content.updatecolumns.idx, + breadcrumbs: store.breadcrumbs.items, + total: store.search.total, + searchRequest: { + loading: store.search.loading, + loaded: store.search.loaded, + }, + pathname: props.location.pathname, + action: store.clipboard.action, + source: store.clipboard.source, + clipboardRequest: store.clipboard.request, + deleteRequest: store.content.delete, + updateRequest: store.content.update, + objectActions: store.actions.actions.object, + orderRequest: store.content.order, + }; + }, + { + searchContent, + cut, + copy, + copyContent, + deleteContent, + listActions, + moveContent, + orderContent, + sortContent, + updateColumnsContent, + getContent, + }, + ), + asyncConnect([ + { + key: 'actions', + // Dispatch async/await to make the operation synchronous, otherwise it returns + // before the promise is resolved + promise: async ({ location, store: { dispatch } }) => + await dispatch(listActions(getBaseUrl(location.pathname))), + }, + ]), + injectLazyLibs(['toastify', 'reactDnd', 'reactDndHtml5Backend']), +)(DragDropConnector); diff --git a/frontend/packages/kitconcept-core/src/components/Contents/DropZoneContent.jsx b/frontend/packages/kitconcept-core/src/components/Contents/DropZoneContent.jsx new file mode 100644 index 00000000..149ef4d8 --- /dev/null +++ b/frontend/packages/kitconcept-core/src/components/Contents/DropZoneContent.jsx @@ -0,0 +1,307 @@ +import { useState, useEffect } from 'react'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; +import { + Button, + Modal, + Table, + Input, + Dimmer, + Progress, +} from 'semantic-ui-react'; +import cx from 'classnames'; +import filesize from 'filesize'; +import { readAsDataURL } from 'promise-file-reader'; + +import { createContent } from '@plone/volto/actions/content/content'; +import { usePrevious } from '@plone/volto/helpers/Utils/usePrevious'; +import { validateFileUploadSize } from '@plone/volto/helpers/FormValidation/FormValidation'; +import Icon from '@plone/volto/components/theme/Icon/Icon'; +import uploadSVG from '@plone/volto/icons/upload.svg'; +import clearSVG from '@plone/volto/icons/clear.svg'; +import FormattedRelativeDate from '@plone/volto/components/theme/FormattedDate/FormattedRelativeDate'; +import Image from '@plone/volto/components/theme/Image/Image'; + +const SUBREQUEST = 'batch-upload'; + +const messages = defineMessages({ + cancel: { + id: 'Cancel', + defaultMessage: 'Cancel', + }, + upload: { + id: '{count, plural, one {Upload {count} file} other {Upload {count} files}}', + defaultMessage: + '{count, plural, one {Upload {count} file} other {Upload {count} files}}', + }, + filesUploaded: { + id: 'Files uploaded: {uploadedFiles}', + defaultMessage: 'Files uploaded: {uploadedFiles}', + }, + totalFilesToUpload: { + id: 'Total files to upload: {totalFiles}', + defaultMessage: 'Total files to upload: {totalFiles}', + }, +}); + +const DropZoneContent = (props) => { + const { onOk, onCancel, pathname, children } = props; + const [isDragOver, setIsDragOver] = useState(false); + const [showModal, setShowModal] = useState(false); + const [droppedFiles, setDroppedFiles] = useState([]); + const [totalFiles, setTotalFiles] = useState(0); + + const intl = useIntl(); + const dispatch = useDispatch(); + + const request = useSelector( + (state) => state.content.subrequests?.[SUBREQUEST] || {}, + shallowEqual, + ); + + const uploadedFiles = useSelector((state) => state.content.uploadedFiles); + const prevrequestloading = usePrevious(request.loading); + + useEffect(() => { + if (prevrequestloading && request.loaded) { + onOk(); + setDroppedFiles([]); + } + }, [prevrequestloading, request.loaded, onOk]); + + const handleDragEnter = (e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(true); + }; + + const handleDragLeave = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!e.currentTarget.contains(e.relatedTarget)) { + setIsDragOver(false); + } + }; + + const handleDragOver = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const onDrop = async (e) => { + setIsDragOver(false); + const newFiles = Array.from(e.dataTransfer.files); + const validFiles = []; + for (let i = 0; i < newFiles.length; i++) { + if (validateFileUploadSize(newFiles[i], intl.formatMessage)) { + await readAsDataURL(newFiles[i]).then((data) => { + const fields = data.match(/^data:(.*);(.*),(.*)$/); + newFiles[i].preview = fields[0]; + }); + validFiles.push(newFiles[i]); + } + } + setDroppedFiles(droppedFiles.concat(validFiles)); + setTotalFiles(validFiles.length); + setShowModal(true); + }; + + const handleCloseModal = () => { + setShowModal(false); + onCancel(); + setDroppedFiles([]); + setTotalFiles(0); + }; + + const onSubmit = () => { + Promise.all(droppedFiles.map((file) => readAsDataURL(file))).then( + (dataUrls) => { + dispatch( + createContent( + pathname, + droppedFiles.map((file, index) => { + const fields = dataUrls[index].match(/^data:(.*);(.*),(.*)$/); + const image = fields[1].split('/')[0] === 'image'; + return { + '@type': image ? 'Image' : 'File', + title: file.name, + [image ? 'image' : 'file']: { + data: fields[3], + encoding: fields[2], + 'content-type': fields[1], + filename: file.name, + }, + }; + }), + SUBREQUEST, + ), + ); + }, + ); + handleCloseModal(); + }; + const onRemoveFile = (index) => { + const updatedFiles = droppedFiles.filter((file, i) => i !== index); + setDroppedFiles(updatedFiles); + setTotalFiles(updatedFiles.length); + }; + + const onChangeFileName = (e, index) => { + let copyOfFiles = [...droppedFiles]; + let originalFile = droppedFiles[index]; + let newFile = new File([originalFile], e.target.value, { + type: originalFile.type, + }); + + newFile.preview = originalFile.preview; + newFile.path = e.target.value; + copyOfFiles[index] = newFile; + setDroppedFiles(copyOfFiles); + }; + + return ( + <> +
+ {children} + {isDragOver && ( +
+
+ +

Drop files here to upload

+

Release to add file(s) to this folder

+
+
+ )} +
+ + Upload Files ({droppedFiles.length}) + +
+ + {intl.formatMessage(messages.filesUploaded, { + uploadedFiles, + })} +
+ {intl.formatMessage(messages.totalFilesToUpload, { + totalFiles, + })} +
+
+
+ + {droppedFiles.length > 0 && ( + + + + + + + + + + + + + + + + + + + + {droppedFiles.map((file, index) => ( + + + onChangeFileName(e, index)} + /> + + + {file.lastModifiedDate && ( + + )} + + {filesize(file.size, { round: 0 })} + + {file.type.split('/')[0] === 'image' && ( + + )} + + + onRemoveFile(index)} + /> + + + ))} + +
+ )} +
+ + {droppedFiles.length > 0 && ( +