@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
315 lines (313 loc) • 10.8 kB
JavaScript
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { alpha } from '@mui/material';
import { makeStyles } from 'tss-react/mui';
import SearchBar from '../SearchBar/SearchBar';
import { useDispatch, useSelector } from 'react-redux';
import { fromEvent, interval } from 'rxjs';
import { filter, map, share, switchMap, takeUntil, tap } from 'rxjs/operators';
import { getHostToGuestBus } from '../../utils/subjects';
import {
assetDragEnded,
assetDragStarted,
fetchAssetsPanelItems,
setPreviewEditMode
} from '../../state/actions/preview';
import MediaCard from '../MediaCard/MediaCard';
import DragIndicatorRounded from '@mui/icons-material/DragIndicatorRounded';
import EmptyState from '../EmptyState/EmptyState';
import UploadIcon from '@mui/icons-material/Publish';
import { pluckProps } from '../../utils/object';
import { uploadDataUrl } from '../../services/content';
import palette from '../../styles/palette';
import { useSelection } from '../../hooks/useSelection';
import { useActiveSiteId } from '../../hooks/useActiveSiteId';
import { useDebouncedInput } from '../../hooks/useDebouncedInput';
import Pagination from '../Pagination';
import { LoadingState } from '../LoadingState';
import { ApiResponseErrorState } from '../ApiResponseErrorState';
import { showPreviewDialog } from '../../state/actions/dialogs';
const translations = defineMessages({
previewAssetsPanelTitle: {
id: 'previewAssetsPanel.title',
defaultMessage: 'Assets'
},
itemsPerPage: {
id: 'previewAssetsPanel.itemsPerPage',
defaultMessage: 'Items per page:'
},
noResults: {
id: 'previewAssetsPanel.noResults',
defaultMessage: ' No results found.'
},
previousPage: {
id: 'pagination.previousPage',
defaultMessage: 'Previous page'
},
nextPage: {
id: 'pagination.nextPage',
defaultMessage: 'Next page'
},
retrieveAssets: {
id: 'previewAssetsPanel.retrieveAssets',
defaultMessage: 'Retrieving Project Assets'
}
});
const assetsPanelStyles = makeStyles()((theme) => ({
assetsPanelWrapper: {
padding: theme.spacing(2)
},
search: {
padding: '15px 15px 0 15px'
},
card: {
cursor: 'move',
marginBottom: '16px'
},
noResultsImage: {
width: '150px'
},
noResultsTitle: {
fontSize: 'inherit',
marginTop: '10px'
},
uploadOverlay: {
position: 'absolute',
background: alpha(palette.black, 0.9),
top: 0,
bottom: 0,
left: 0,
right: 0,
display: 'flex',
justifyContent: 'center',
alignContent: 'center',
zIndex: 2
},
uploadIcon: {
fontSize: '8em',
color: palette.gray.light5,
margin: 'auto'
},
noScroll: {
overflow: 'hidden'
}
}));
export function PreviewAssetsPanel() {
var _a;
const { classes } = assetsPanelStyles();
const initialKeyword = useSelection((state) => state.preview.assets.query.keywords);
const [keyword, setKeyword] = useState(initialKeyword);
const [dragInProgress, setDragInProgress] = useState(false);
const site = useActiveSiteId();
const hostToGuest$ = getHostToGuestBus();
const dispatch = useDispatch();
const editMode = useSelection((state) => state.preview.editMode);
const assets = useSelection((state) => state.preview.assets);
useEffect(() => {
if (site && assets.isFetching === null) {
dispatch(fetchAssetsPanelItems({}));
}
}, [assets, dispatch, site]);
const { guestBase, xsrfArgument } = useSelector((state) => state.env);
const { formatMessage } = useIntl();
const elementRef = useRef();
const onDragStart = (mediaItem) => {
if (!editMode) {
dispatch(setPreviewEditMode({ editMode: true }));
}
hostToGuest$.next({
type: assetDragStarted.type,
payload: mediaItem
});
};
const onDragEnd = () => hostToGuest$.next(assetDragEnded());
const onDragDrop = useCallback(
(e) => {
const file = e.dataTransfer.files[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onloadend = function () {
uploadDataUrl(
site,
Object.assign(Object.assign({}, pluckProps(file, 'name', 'type')), { dataUrl: reader.result }),
'/static-assets/images/',
xsrfArgument
).subscribe({
complete() {
dispatch(fetchAssetsPanelItems({}));
}
});
};
reader.readAsDataURL(file);
setDragInProgress(false);
},
[xsrfArgument, dispatch, site]
);
useEffect(() => {
const subscription = fromEvent(elementRef.current, 'dragenter')
.pipe(
filter((e) => {
var _a;
return (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.types.includes('Files');
})
)
.subscribe((e) => {
e.preventDefault();
e.stopPropagation();
setDragInProgress(true);
});
return () => subscription.unsubscribe();
}, []);
useEffect(() => {
if (dragInProgress) {
const dragover$ = fromEvent(elementRef.current, 'dragover').pipe(
tap((e) => {
e.preventDefault();
e.stopPropagation();
}),
share()
);
const dragoverSubscription = dragover$.subscribe();
const dragleaveSubscription = fromEvent(elementRef.current, 'dragleave')
.pipe(
switchMap(() => interval(100).pipe(takeUntil(dragover$))),
map(() => false)
)
.subscribe(setDragInProgress);
const dropSubscription = fromEvent(elementRef.current, 'drop')
.pipe(
filter((e) => {
var _a;
return (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.types.includes('Files');
})
)
.subscribe((e) => {
e.preventDefault();
e.stopPropagation();
onDragDrop(e);
});
return () => {
dragoverSubscription.unsubscribe();
dragleaveSubscription.unsubscribe();
dropSubscription.unsubscribe();
};
}
}, [dragInProgress, onDragDrop]);
const onSearch = useCallback((keywords) => dispatch(fetchAssetsPanelItems({ keywords, offset: 0 })), [dispatch]);
const onSearch$ = useDebouncedInput(onSearch, 400);
function onPageChanged(newPage) {
dispatch(fetchAssetsPanelItems({ offset: newPage }));
}
function onRowsPerPageChange(e) {
dispatch(fetchAssetsPanelItems({ offset: 0, limit: e.target.value }));
}
function handleSearchKeyword(keyword) {
setKeyword(keyword);
onSearch$.next(keyword);
}
return React.createElement(
'div',
{ className: dragInProgress ? classes.noScroll : null },
React.createElement(
'div',
{ ref: elementRef },
React.createElement(
'div',
{ className: classes.search },
React.createElement(SearchBar, {
showActionButton: Boolean(keyword),
onChange: handleSearchKeyword,
keyword: keyword
})
),
assets.error
? React.createElement(ApiResponseErrorState, { error: assets.error })
: assets.isFetching
? React.createElement(LoadingState, { title: formatMessage(translations.retrieveAssets) })
: assets.page[assets.pageNumber]
? React.createElement(
React.Fragment,
null,
dragInProgress &&
React.createElement(
'div',
{ className: classes.uploadOverlay },
React.createElement(UploadIcon, { style: { pointerEvents: 'none' }, className: classes.uploadIcon })
),
React.createElement(Pagination, {
count: assets.count,
rowsPerPage: assets.query.limit,
page: assets.pageNumber,
onPageChange: (e, page) => onPageChanged(page * assets.query.limit),
onRowsPerPageChange: onRowsPerPageChange
}),
React.createElement(
'div',
{ className: classes.assetsPanelWrapper },
(_a = assets.page[assets.pageNumber]) === null || _a === void 0
? void 0
: _a.map((id) => {
const item = assets.byId[id];
return React.createElement(MediaCard, {
key: item.path,
item: item,
previewAppBaseUri: guestBase,
avatar: React.createElement(DragIndicatorRounded, null),
classes: { root: classes.card },
onDragStart: () => onDragStart(item),
onDragEnd: () => onDragEnd(),
onPreview: () =>
dispatch(
showPreviewDialog({
// TODO: check if it's image or video
type: 'image',
title: item.name,
url: item.path
})
)
});
}),
assets.count === 0 &&
React.createElement(EmptyState, {
title: formatMessage(translations.noResults),
classes: { image: classes.noResultsImage, title: classes.noResultsTitle }
})
)
)
: React.createElement(React.Fragment, null)
)
);
}
export default PreviewAssetsPanel;