Source: plugins/SaveAs.jsx

/*
 * Copyright 2020, GeoSolutions Sas.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree.
 */

import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { createPlugin } from '@mapstore/framework/utils/PluginsUtils';
import { setControlProperty } from '@mapstore/framework/actions/controls';
import Message from '@mapstore/framework/components/I18N/Message';
import { mapInfoSelector } from '@mapstore/framework/selectors/map';
import { userSelector } from '@mapstore/framework/selectors/security';
import Button from '@js/components/Button';
import {
    saveContent,
    clearSave
} from '@js/actions/gnsave';
import controls from '@mapstore/framework/reducers/controls';
import gnresource from '@js/reducers/gnresource';
import gnsave from '@js/reducers/gnsave';
import gnsaveEpics from '@js/epics/gnsave';
import SaveModal from '@js/plugins/save/SaveModal';
import {
    canAddResource,
    getResourceId,
    getResourceData,
    isNewResource,
    getResourceDirtyState
} from '@js/selectors/resource';
import { ProcessTypes } from '@js/utils/ResourceServiceUtils';
import { canCopyResource } from '@js/utils/ResourceUtils';
import { processResources } from '@js/actions/gnresource';
import { getCurrentResourceCopyLoading } from '@js/selectors/resourceservice';
import Dropdown from '@js/components/Dropdown';
import FaIcon from '@js/components/FaIcon';
import withPrompt from '@js/plugins/save/withPrompt';

function SaveAs({
    resources,
    onSave,
    onCopy,
    isNew,
    closeOnSave,
    labelId,
    ...props
}) {
    return (
        <SaveModal
            {...props}
            hideDescription={!isNew}
            copy={!isNew}
            // add key to reset the component when a new resource is returned
            key={props?.resource?.pk || 'new'}
            labelId={labelId || 'save'}
            onSave={(id, metadata, reload) => {
                if (isNew) {
                    // only new resource follow the sync save
                    onSave(id, metadata, reload);
                } else {
                    // existing resource are using the async copy workflow
                    onCopy([
                        {
                            ...props?.resource,
                            title: metadata.name || props?.resource?.title
                        }
                    ]);
                }
                // catalogue page must close the clone modal as soon as the user click on clone
                if (closeOnSave) {
                    props.onClose();
                }
            }}
        />
    );
}

const SaveAsPlugin = connect(
    createSelector([
        state => state?.controls?.[ProcessTypes.COPY_RESOURCE]?.value,
        mapInfoSelector,
        state => state?.gnresource?.loading,
        state => state?.gnsave?.saving,
        state => state?.gnsave?.error,
        state => state?.gnsave?.success,
        getResourceId,
        isNewResource,
        getCurrentResourceCopyLoading
    ], (resources, mapInfo, loading, saving, error, success, contentId, isNew, processLoading) => ({
        enabled: !!resources,
        contentId: contentId || mapInfo?.id,
        resource: resources?.[0],
        loading: processLoading || loading,
        saving,
        error,
        success,
        isNew
    })),
    {
        onClose: setControlProperty.bind(null, ProcessTypes.COPY_RESOURCE, 'value', undefined),
        onSave: saveContent,
        onCopy: processResources.bind(null, ProcessTypes.COPY_RESOURCE),
        onClear: clearSave
    }
)(SaveAs);

function SaveAsButton({
    onClick,
    variant,
    size,
    resource,
    dirtyState,
    disabled
}) {

    return (
        <Button
            variant={dirtyState ? 'warning' : (variant || "primary")}
            size={size}
            disabled={disabled}
            onClick={() => onClick([ resource ])}
        >
            <Message msgId="saveAs"/>
        </Button>
    );
}

const canCopyResourceFunction = (state) => {
    return (resource) => {
        const user = userSelector(state);
        if (!user) {
            return false;
        }
        const isResourceNew = isNewResource(state);
        const canAdd = canAddResource(state);
        if (isResourceNew && canAdd) {
            return true;
        }
        return canCopyResource(resource, user);
    };
};

const isDisabledByDirtyState = (dirtyState) => {
    return typeof dirtyState === 'object' ? !!dirtyState : false;
};

const ConnectedSaveAsButton = connect(
    createSelector(
        getResourceData,
        getResourceDirtyState,
        canCopyResourceFunction,
        (resource, dirtyState, canCopy) => ({
            enabled: !!canCopy(resource),
            resource,
            dirtyState: !isDisabledByDirtyState(dirtyState) && !!dirtyState,
            disabled: isDisabledByDirtyState(dirtyState)
        })
    ),
    {
        onClick: setControlProperty.bind(null, ProcessTypes.COPY_RESOURCE, 'value')
    }
)(withPrompt(SaveAsButton));

function CopyMenuItem({
    resource,
    canCopy,
    onCopy
}) {
    if (!canCopy(resource)) {
        return null;
    }
    return (
        <Dropdown.Item
            onClick={() =>
                onCopy([resource])
            }
        >
            <FaIcon name="copy" />{' '}
            <Message msgId="gnviewer.clone" />
        </Dropdown.Item>
    );
}

const ConnectedMenuItem = connect(
    createSelector([
        canCopyResourceFunction
    ], (canCopy) => ({
        canCopy
    })),
    {
        onCopy: setControlProperty.bind(null, ProcessTypes.COPY_RESOURCE, 'value')
    }
)((CopyMenuItem));

/**
* @module SaveAs
*/

/**
 * enable button or menu item to clone a specific resource
 * @name SaveAs
 * @prop {boolean} closeOnSave close the modal after clicking on save button
 * @example
 * {
 *  "name": "SaveAs",
 *  "cfg": {
 *      "closeOnSave": true
 *  }
 * }
 */
export default createPlugin('SaveAs', {
    component: SaveAsPlugin,
    containers: {
        ActionNavbar: {
            name: 'SaveAs',
            Component: ConnectedSaveAsButton
        },
        ResourcesGrid: {
            name: ProcessTypes.COPY_RESOURCE,
            target: 'cardOptions',
            Component: ConnectedMenuItem
        }
    },
    epics: {
        ...gnsaveEpics
    },
    reducers: {
        gnresource,
        gnsave,
        controls
    }
});