Skip to content

Commit

Permalink
Implement direct download action buttons (#844)
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidQuartz authored Feb 23, 2022
1 parent 3fbd164 commit 5df0813
Show file tree
Hide file tree
Showing 24 changed files with 298 additions and 63 deletions.
17 changes: 17 additions & 0 deletions geonode_mapstore_client/client/js/actions/gnresource.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const RESET_GEO_LIMITS = 'GEONODE:RESET_GEO_LIMITS';
export const PROCESS_RESOURCES = 'GEONODE:PROCESS_RESOURCES';
export const SET_RESOURCE_THUMBNAIL = 'GEONODE_SET_RESOURCE_THUMBNAIL';
export const ENABLE_MAP_THUMBNAIL_VIEWER = 'GEONODE_ENABLE_MAP_THUMBNAIL_VIEWER';
export const DOWNLOAD_RESOURCE = 'GEONODE_DOWNLOAD_RESOURCE';
export const DOWNLOAD_COMPLETE = 'GEONODE_DOWNLOAD_COMPLETE';


/**
* Actions for GeoNode resource
Expand Down Expand Up @@ -280,3 +283,17 @@ export function processResources(processType, resources, redirectTo) {
redirectTo
};
}

export function downloadResource(resource) {
return {
type: DOWNLOAD_RESOURCE,
resource
};
}

export function downloadComplete(resource) {
return {
type: DOWNLOAD_COMPLETE,
resource
};
}
12 changes: 12 additions & 0 deletions geonode_mapstore_client/client/js/api/geonode/v2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,17 @@ export const copyResource = (resource) => {
.then(({ data }) => data);
};

export const downloadResource = (resource) => {
const url = resource.download_url || resource.href;
return axios.get(url, {
responseType: 'blob',
headers: {
'Content_type': 'application/jsson'
}
})
.then(({ data, headers }) => ({output: data, headers}));
};

export const getPendingUploads = () => {
return axios.get(parseDevHostname(endpoints[UPLOADS]), {
params: {
Expand Down Expand Up @@ -813,6 +824,7 @@ export default {
updateCompactPermissionsByPk,
deleteResource,
copyResource,
downloadResource,
getDatasets,
getPendingUploads,
getProcessedUploadsById,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ function ActionButtons({
actions,
onAction,
resource,
buildHrefByTemplate
buildHrefByTemplate,
onDownload
}) {
return (
<div className="gn-resource-action-buttons">
Expand All @@ -31,14 +32,14 @@ function ActionButtons({
</Dropdown.Toggle>
<Dropdown.Menu className={`gn-card-dropdown`}>
{options.map((opt) => {
if (opt.type === 'button' && actions[opt.action]) {
if ((opt.type === 'button' && actions[opt.action]) || opt.action === 'download') {
return (
(opt.action !== 'copy' || resource?.is_copyable) && <Dropdown.Item
((opt.action === 'download' && resource.download_url) || (opt.action !== 'copy' && opt.action !== 'download') || resource?.is_copyable) && <Dropdown.Item
key={opt.action}
onClick={() =>
onAction(actions[opt.action], [
opt.action !== 'download' ? onAction(actions[opt.action], [
resource
])
]) : onDownload(resource)
}
>
<FaIcon name={opt.icon} />{' '}
Expand Down
12 changes: 10 additions & 2 deletions geonode_mapstore_client/client/js/components/CardGrid/CardGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ const Cards = withResizeDetector(({
buildHrefByTemplate,
options,
actions,
onAction
onAction,
onDownload,
downloading
}) => {

const width = containerWidth || detectedWidth;
Expand Down Expand Up @@ -117,6 +119,8 @@ const Cards = withResizeDetector(({
onAction={onAction}
loading={isProcessing}
readOnly={isDeleted || isProcessing}
onDownload={onDownload}
downloading={downloading?.find((download) => download.pk === resource.pk) ? true : false}
/>
</li>
);
Expand All @@ -142,7 +146,9 @@ const CardGrid = ({
scrollContainer,
actions,
onAction,
onControl
onControl,
onDownload,
downloading
}) => {

useInfiniteScroll({
Expand Down Expand Up @@ -178,6 +184,8 @@ const CardGrid = ({
options={cardOptions}
buildHrefByTemplate={buildHrefByTemplate}
actions={actions}
onDownload={onDownload}
downloading={downloading}
onAction={(action, payload) => {
if (action.isControlled) {
onControl(action.processType, 'value', payload);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ function DetailsPanel({
linkHref,
sectionStyle,
loading,
downloading,
getTypesInfo,
editTitle,
editAbstract,
Expand All @@ -202,7 +203,9 @@ function DetailsPanel({
resourceThumbnailUpdating,
initialBbox,
enableMapViewer,
onClose
onClose,
onAction,
canDownload
}) {
const detailsContainerNode = useRef();
const isMounted = useRef();
Expand Down Expand Up @@ -246,7 +249,8 @@ function DetailsPanel({
const embedUrl = resource?.embed_url && formatEmbedUrl(resource);
const detailUrl = resource?.pk && formatDetailUrl(resource);
const resourceCanPreviewed = resource?.pk && canPreviewed && canPreviewed(resource);
const documentDownloadUrl = (resource?.href && resource?.href.includes('download')) ? resource?.href : undefined;
const downloadUrl = (resource?.href && resource?.href.includes('download')) ? resource?.href
: (resource?.download_url && canDownload) ? resource?.download_url : undefined;
const metadataDetailUrl = resource?.pk && getMetadataDetailUrl(resource);

const validateDataType = (data) => {
Expand Down Expand Up @@ -516,7 +520,7 @@ function DetailsPanel({

<div className="gn-details-panel-content-text">
<div className="gn-details-panel-title" >
<span className="gn-details-panel-title-icon" ><FaIcon name={icon} /> </span> <EditTitle disabled={!activeEditMode} tagName="h1" title={resource?.title} onEdit={editTitle} >
<span className="gn-details-panel-title-icon" >{!downloading ? <FaIcon name={icon} /> : <Spinner />} </span> <EditTitle disabled={!activeEditMode} tagName="h1" title={resource?.title} onEdit={editTitle} >

</EditTitle>

Expand All @@ -530,9 +534,9 @@ function DetailsPanel({
<FaIcon name={favorite ? 'star' : 'star-o'} />
</Button>
}
{documentDownloadUrl &&
{downloadUrl &&
<Button variant="default"
href={documentDownloadUrl} >
onClick={() => onAction(resource)} >
<FaIcon name="download" />
</Button>}

Expand Down Expand Up @@ -602,7 +606,8 @@ DetailsPanel.defaultProps = {
onResourceThumbnail: () => '#',
width: 696,
getTypesInfo: getResourceTypesInfo,
isThumbnailChanged: false
isThumbnailChanged: false,
onAction: () => {}
};

export default DetailsPanel;
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ const Cards = ({
options,
onResize,
actions,
onAction
onAction,
onDownload,
downloading
}) => {
const width = detectedWidth || containerWidth;
const margin = 24;
Expand Down Expand Up @@ -93,6 +95,8 @@ const Cards = ({
featured
actions={actions}
onAction={onAction}
onDownload={onDownload}
downloading={downloading?.find((download) => download.pk === resource.pk) ? true : false}
/>
</li>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ const FeaturedList = withResizeDetector(({
onLoad,
width,
onControl,
onAction
onAction,
onDownload,
downloading
}) => {

const [count, setCount] = useState();
Expand Down Expand Up @@ -73,6 +75,8 @@ const FeaturedList = withResizeDetector(({
onAction(action.processType, payload, action.redirectTo);
}
}}
onDownload={onDownload}
downloading={downloading}
/>
<div className="gn-card-grid-pagination featured-list">

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ const ResourceCard = forwardRef(({
className,
loading,
featured,
onClick
onClick,
downloading,
onDownload
}, ref) => {
const res = data;
const types = getTypesInfo();
Expand Down Expand Up @@ -80,6 +82,7 @@ const ResourceCard = forwardRef(({
options={options}
readOnly={readOnly}
onAction={onAction}
onDownload={onDownload}
/>
)}
<div className={`card-resource-${layoutCardsStyle}`}>
Expand All @@ -96,7 +99,7 @@ const ResourceCard = forwardRef(({
<div className="card-body">
<div className="card-title">
<div>
{icon && !loading && (
{(icon && !loading && !downloading) && (
<>
<ALink
readOnly={readOnly}
Expand All @@ -111,7 +114,7 @@ const ResourceCard = forwardRef(({
</ALink>
</>
)}
{loading && <Spinner />}
{(loading || downloading) && <Spinner />}
<ALink
className={
featured
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import {
STOP_ASYNC_PROCESS,
startAsyncProcess
} from '@js/actions/resourceservice';
import { gnMonitorAsyncProcesses } from '../resourceservice';
import { gnMonitorAsyncProcesses, gnDownloadResource } from '../resourceservice';
import {
SHOW_NOTIFICATION
} from '@mapstore/framework/actions/notifications';
import { DOWNLOAD_COMPLETE, downloadResource } from '@js/actions/gnresource';

let mockAxios;

Expand Down Expand Up @@ -83,4 +84,29 @@ describe('resourceservice epics', () => {
testState
);
});

it('test gnDownloadResource', (done) => {
const testState = {
resourceservice: {}
};
const actionsCount = 1;
mockAxios.onGet().reply(() => [200, {output: {}, headers: { 'content-type': 'application/zip' } }]);
testEpic(
gnDownloadResource,
actionsCount,
downloadResource({ pk: 1, title: 'test resource' }),
(actions) => {
try {
expect(actions.map(({ type }) => type))
.toEqual([
DOWNLOAD_COMPLETE
]);
} catch (e) {
done(e);
}
done();
},
testState
);
});
});
41 changes: 33 additions & 8 deletions geonode_mapstore_client/client/js/epics/resourceservice.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { Observable } from 'rxjs';
import axios from '@mapstore/framework/libs/ajax';
import { saveAs } from 'file-saver';

import {
START_ASYNC_PROCESS,
Expand All @@ -23,9 +24,10 @@ import {
import { isProcessCompleted } from '@js/selectors/resourceservice';
import {
deleteResource,
copyResource
copyResource,
downloadResource
} from '@js/api/geonode/v2';
import { PROCESS_RESOURCES } from '@js/actions/gnresource';
import { PROCESS_RESOURCES, DOWNLOAD_RESOURCE, downloadComplete } from '@js/actions/gnresource';
import { setControlProperty } from '@mapstore/framework/actions/controls';
import { push } from 'connected-react-router';
import {
Expand All @@ -37,7 +39,7 @@ export const gnMonitorAsyncProcesses = (action$, store) => {
.flatMap((action) => {
const { status_url: statusUrl } = action?.payload?.output || {};
if (!statusUrl || action?.payload?.error) {
return action?.payload?.error ? Observable.of(stopAsyncProcess({ ...action.payload, completed: true }), errorNotification({ title: 'gnviewer.invalidUploadMessageError', message: 'gnviewer.cannotCloneResource' }))
return action?.payload?.error ? Observable.of(stopAsyncProcess({ ...action.payload, completed: true }), errorNotification({ title: 'gnviewer.invalidUploadMessageError', message: 'gnviewer.cannotPerfomAction' }))
: Observable.of(stopAsyncProcess({ ...action.payload, completed: true }));
}
return Observable
Expand Down Expand Up @@ -69,10 +71,10 @@ export const gnProcessResources = (action$) =>
// all the processes must be listened for this reason we should use flatMap instead of switchMap
.flatMap((action) => {
return Observable.defer(() => axios.all(
action.resources.map(resource =>
processAPI[action.processType](resource)
.then(output => ({ resource, output, processType: action.processType }))
.catch((error) => ({ resource, error: error?.data?.detail || error?.statusText || error?.message || true, processType: action.processType }))
action.resources.map((resource) => processAPI[action.processType](resource)
.then((output) => ({ resource, output, processType: action.processType })
)
.catch((error) => ({ resource, error: error?.data?.detail || error?.statusText || error?.message || true, processType: action.processType }))
)
))
.switchMap((processes) => {
Expand All @@ -88,7 +90,30 @@ export const gnProcessResources = (action$) =>
.startWith(setControlProperty(action.processType, 'loading', true));
});

export const gnDownloadResource = (action$) =>
action$.ofType(DOWNLOAD_RESOURCE)
.switchMap((action) => {
const resource = action?.resource;
return Observable.defer(() => downloadResource(resource)
.then(({ output, headers }) => {
saveAs(new Blob([output], { type: headers?.['content-type'] }), resource.title);
return { resource };
})
.catch((error) => ({ resource, error: error?.data?.detail || error?.statusText || error?.message || true }))
)
.switchMap((downloaded) => {
const { error } = downloaded || {};
if (error) {
return Observable.of(downloadComplete({ ...downloaded.resource }), errorNotification({ title: 'gnviewer.invalidUploadMessageError', message: 'gnviewer.cannotPerfomAction' }));
}
return Observable.of(
downloadComplete({ ...downloaded.resource })
);
});
});

export default {
gnMonitorAsyncProcesses,
gnProcessResources
gnProcessResources,
gnDownloadResource
};
Loading

0 comments on commit 5df0813

Please sign in to comment.