UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

531 lines (529 loc) 21.6 kB
/* * 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-2023 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 { DialogBody } from '../DialogBody'; import Box from '@mui/material/Box'; import { DialogFooter } from '../DialogFooter'; import Button from '@mui/material/Button'; import { FormattedMessage, useIntl } from 'react-intl'; import PrimaryButton from '../PrimaryButton'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import Grid from '@mui/material/Grid'; import Typography from '@mui/material/Typography'; import IconButton from '@mui/material/IconButton'; import EditIcon from '@mui/icons-material/Edit'; import { keyframes } from 'tss-react'; import { fadeIn } from 'react-animations'; import { ApiResponseErrorState } from '../ApiResponseErrorState'; import { CreateSiteDialogLoader } from '../CreateSiteDialog'; import { duplicate, fetchAll, fetchLegacySite } from '../../services/sites'; import { setSiteCookie } from '../../utils/auth'; import { getSystemLink } from '../../utils/system'; import useEnv from '../../hooks/useEnv'; import { nnou } from '../../utils/object'; import useMount from '../../hooks/useMount'; import { cleanupGitBranch, cleanupSiteId, getSiteIdFromSiteName, siteIdExist, siteNameExist } from '../CreateSiteDialog/utils'; import FormControl from '@mui/material/FormControl'; import InputLabel from '@mui/material/InputLabel'; import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; import FormHelperText from '@mui/material/FormHelperText'; import BaseSiteForm from '../CreateSiteDialog/BaseSiteForm'; import FormControlLabel from '@mui/material/FormControlLabel'; import Switch from '@mui/material/Switch'; import Alert from '@mui/material/Alert'; import useUpdateRefs from '../../hooks/useUpdateRefs'; import { useDispatch } from 'react-redux'; import { showSystemNotification } from '../../state/actions/system'; export function DuplicateSiteDialogContainer(props) { const { site, setSite, handleClose, onGoBack, isSubmitting, onSubmittingAndOrPendingChange } = props; const [error, setError] = useState(null); const { authoringBase, useBaseDomain } = useEnv(); const fieldsErrorsLookup = useMemo( () => ({ sourceSiteId: !site.sourceSiteId, siteName: !site.siteName || site.siteNameExist, siteId: !site.siteId || site.siteIdExist || site.invalidSiteId, description: false, gitBranch: false }), [site.invalidSiteId, site.siteId, site.siteIdExist, site.siteName, site.siteNameExist, site.sourceSiteId] ); const [sourceSiteHasBlobStores, setSourceSiteHasBlobStores] = useState(null); const primaryButtonRef = useRef(null); const siteDuplicateSubscription = useRef(); const [sites, setSites] = useState(null); const fnRefs = useUpdateRefs({ onSubmittingAndOrPendingChange, handleClose }); const mountedRef = useRef(true); const dispatch = useDispatch(); const { formatMessage } = useIntl(); const validateForm = () => { return !( !site.sourceSiteId || !site.siteId || site.siteIdExist || !site.siteName || site.siteNameExist || site.invalidSiteId ); }; const duplicateSite = () => { onSubmittingAndOrPendingChange({ isSubmitting: true }); siteDuplicateSubscription.current = duplicate({ sourceSiteId: site.sourceSiteId, siteId: site.siteId, siteName: site.siteName, description: site.description, sandboxBranch: site.gitBranch, ...(sourceSiteHasBlobStores && { readOnlyBlobStores: site.readOnlyBlobStores }) }).subscribe({ next: () => { siteDuplicateSubscription.current = null; if (mountedRef.current) { fnRefs.current.onSubmittingAndOrPendingChange({ isSubmitting: false }); fnRefs.current.handleClose(); setSiteCookie(site.siteId, useBaseDomain); window.location.href = getSystemLink({ systemLinkId: 'preview', authoringBase, site: site.siteId, page: '/' }); } }, error: ({ response }) => { siteDuplicateSubscription.current = null; if (mountedRef.current) { setError(response.response); fnRefs.current.onSubmittingAndOrPendingChange({ isSubmitting: false }); } else { console.error('Error duplicating site', response); dispatch( showSystemNotification({ message: formatMessage({ defaultMessage: `Error creating site "{name}"` }, { name: site.siteName }), options: { variant: 'error' } }) ); } } }); }; const handleBack = () => { let back = site.selectedView - 1; setSite({ selectedView: back }); }; const handleErrorBack = () => { setError(null); }; const handleFinish = (e) => { e && e.preventDefault(); if (site.selectedView === 0) { const isFormValid = validateForm(); if (isFormValid && !site.siteIdExist) { setSite({ selectedView: 1 }); } else { setSite({ submitted: true }); } } if (site.selectedView === 1) { duplicateSite(); } }; const onKeyPress = (event) => { if (event.key === 'Enter') { handleFinish(null); } }; function checkSites(event) { setSite({ siteIdExist: siteIdExist(sites, event.target.value) }); } function checkSiteNames(event) { setSite({ siteNameExist: siteNameExist(sites, event.target.value) }); } const handleInputChange = (e) => { e.persist?.(); if (e.target.name === 'sourceSiteId') { setSite({ [e.target.name]: e.target.value, ...(sourceSiteHasBlobStores && { readOnlyBlobStores: true }) }); } else if (e.target.name === 'siteId') { const invalidSiteId = e.target.value.startsWith('0') || e.target.value.startsWith('-') || e.target.value.startsWith('_'); const siteId = cleanupSiteId(e.target.value); setSite({ [e.target.name]: siteId, invalidSiteId: invalidSiteId }); } else if (e.target.name === 'siteName') { const currentSiteNameParsed = getSiteIdFromSiteName(site.siteName); // if current siteId has been edited directly (different to siteName processed) // or if siteId is empty -> do not change it. if (site.siteId === currentSiteNameParsed || site.siteId === '') { const siteId = getSiteIdFromSiteName(e.target.value); const invalidSiteId = siteId.startsWith('0') || siteId.startsWith('-') || siteId.startsWith('_'); const siteIdExist = Boolean(sites.find((site) => site.id === siteId)); setSite({ [e.target.name]: e.target.value, siteId, invalidSiteId, siteIdExist }); } else { setSite({ [e.target.name]: e.target.value }); } } else if (e.target.name === 'gitBranch') { setSite({ [e.target.name]: cleanupGitBranch(e.target.value) }); } else if (e.target.type === 'checkbox') { setSite({ [e.target.name]: e.target.checked }); } else { setSite({ [e.target.name]: e.target.value }); } }; useMount(() => { mountedRef.current = true; if (sites === null) { fetchAll({ limit: 1000, offset: 0 }).subscribe(setSites); } return () => { mountedRef.current = false; siteDuplicateSubscription.current?.unsubscribe(); }; }); useEffect(() => { if (site.sourceSiteId) { fetchLegacySite(site.sourceSiteId).subscribe({ next: ({ blobStores }) => { setSourceSiteHasBlobStores(nnou(blobStores) && blobStores.length > 0); }, error: ({ response }) => { setError(response.response); } }); } }, [site?.sourceSiteId, setError]); useEffect(() => { if (primaryButtonRef && primaryButtonRef.current && site.selectedView === 1) { primaryButtonRef.current.focus(); } }, [site.selectedView]); return React.createElement( React.Fragment, null, React.createElement( DialogBody, null, error ? React.createElement( Box, { sx: { height: '100%', display: 'flex', justifyContent: 'center' } }, React.createElement(ApiResponseErrorState, { error: error, onButtonClick: handleErrorBack }) ) : isSubmitting ? React.createElement(CreateSiteDialogLoader, { title: React.createElement(FormattedMessage, { defaultMessage: 'Duplicating Project' }), handleClose: () => { // Avoid cancelling the request if the user sends to background. siteDuplicateSubscription.current = null; handleClose(); onSubmittingAndOrPendingChange({ hasPendingChanges: false, isSubmitting: false }); } }) : React.createElement( Box, { sx: { flexWrap: 'wrap', height: '100%', overflow: 'auto', display: 'flex', padding: '25px', animation: `${keyframes`${fadeIn}`} 1s` } }, site.selectedView === 0 && React.createElement( Box, { component: 'form', sx: { width: '100%', maxWidth: '600px', margin: '0 auto' } }, React.createElement( Grid, { container: true, spacing: 3 }, React.createElement( Grid, { item: true, xs: 12, 'data-field-id': 'sourceSiteId' }, React.createElement( FormControl, { fullWidth: true }, React.createElement( InputLabel, null, React.createElement(FormattedMessage, { defaultMessage: 'Project' }) ), React.createElement( Select, { value: site.sourceSiteId, id: 'sourceSiteId', name: 'sourceSiteId', required: true, label: React.createElement(FormattedMessage, { defaultMessage: 'Project' }), onChange: handleInputChange, error: site.submitted && fieldsErrorsLookup['sourceSiteId'] }, React.createElement(MenuItem, { value: '' }, 'Select project'), sites?.map((siteObj) => React.createElement(MenuItem, { key: siteObj.uuid, value: siteObj.id }, siteObj.name) ) ), site.submitted && !site.sourceSiteId && React.createElement( FormHelperText, { error: true }, React.createElement(FormattedMessage, { defaultMessage: 'Source project is required' }) ) ) ), React.createElement(BaseSiteForm, { inputs: site, fieldsErrorsLookup: fieldsErrorsLookup, checkSites: checkSites, checkSiteNames: checkSiteNames, handleInputChange: handleInputChange, onKeyPress: onKeyPress }), sourceSiteHasBlobStores && React.createElement( Grid, { item: true, xs: 12, 'data-field-id': 'readOnlyBlobStores' }, React.createElement(FormControlLabel, { control: React.createElement(Switch, { name: 'readOnlyBlobStores', checked: site.readOnlyBlobStores, color: 'primary', onChange: handleInputChange }), label: React.createElement(FormattedMessage, { defaultMessage: 'Read-only Blob Stores' }) }), React.createElement( Alert, { severity: site.readOnlyBlobStores ? 'info' : 'warning', icon: false, sx: { mt: 1 } }, React.createElement( Typography, null, React.createElement(FormattedMessage, { defaultMessage: 'Content stored in blob stores is shared between the original site and the copy' }) ) ) ) ) ), site.selectedView === 1 && React.createElement( Grid, { container: true, spacing: 3, sx: { maxWidth: '600px', margin: 'auto' } }, React.createElement( Grid, { item: true, xs: 12 }, React.createElement( Typography, { variant: 'h6', gutterBottom: true, sx: { mb: onGoBack ? 0 : null } }, React.createElement(FormattedMessage, { defaultMessage: 'Creation Strategy' }), onGoBack && React.createElement( IconButton, { onClick: onGoBack, size: 'large', sx: { color: (theme) => theme.palette.primary.main, '& svg': { fontSize: '1.2rem' } } }, React.createElement(EditIcon, null) ) ), React.createElement( 'div', null, React.createElement( Typography, { variant: 'body2', gutterBottom: true }, React.createElement(FormattedMessage, { defaultMessage: 'Duplicate Project' }) ), React.createElement( Typography, { variant: 'body2', gutterBottom: true }, React.createElement( 'span', null, React.createElement(FormattedMessage, { defaultMessage: 'Source project' }), ':', ' ' ), ' ', site.sourceSiteId ) ) ), React.createElement( Grid, { item: true, xs: 12 }, React.createElement( Typography, { variant: 'h6', gutterBottom: true }, React.createElement(FormattedMessage, { defaultMessage: 'Project info' }), React.createElement( IconButton, { onClick: handleBack, size: 'large', sx: { color: (theme) => theme.palette.primary.main, '& svg': { fontSize: '1.2rem' } } }, React.createElement(EditIcon, null) ) ), React.createElement( Typography, { variant: 'body2', gutterBottom: true, noWrap: true }, React.createElement( 'span', null, React.createElement(FormattedMessage, { defaultMessage: 'Project Name' }), ':', ' ' ), site.siteName ), React.createElement( Typography, { variant: 'body2', gutterBottom: true }, React.createElement( 'span', null, React.createElement(FormattedMessage, { defaultMessage: 'Project ID' }), ':', ' ' ), site.siteId ), React.createElement( Typography, { variant: 'body2', gutterBottom: true }, React.createElement( 'span', null, React.createElement(FormattedMessage, { defaultMessage: 'Description' }), ':', ' ' ), site.description ? site.description : React.createElement( 'span', null, '(', React.createElement(FormattedMessage, { defaultMessage: 'No description supplied' }), ')' ) ), React.createElement( Typography, { variant: 'body2', gutterBottom: true }, React.createElement( 'span', null, React.createElement(FormattedMessage, { defaultMessage: 'Git Branch' }), ':' ), ` ${site.gitBranch ? site.gitBranch : 'master'}` ), sourceSiteHasBlobStores && React.createElement( Typography, { variant: 'body2', gutterBottom: true }, React.createElement( 'span', null, React.createElement(FormattedMessage, { defaultMessage: 'Blob Stores mode' }), ':', ' ' ), site.readOnlyBlobStores ? React.createElement(FormattedMessage, { defaultMessage: 'Read-only' }) : React.createElement(FormattedMessage, { defaultMessage: 'Read-write' }) ) ) ) ) ), !isSubmitting && !error && React.createElement( DialogFooter, null, (site.selectedView === 1 || onGoBack) && React.createElement( Button, { color: 'primary', variant: 'outlined', onClick: (e) => { if (onGoBack && site.selectedView === 0) { onGoBack(); } else { handleBack(); } } }, React.createElement(FormattedMessage, { defaultMessage: 'Back' }) ), React.createElement( PrimaryButton, { ref: primaryButtonRef, onClick: handleFinish }, site.selectedView === 0 && React.createElement(FormattedMessage, { defaultMessage: 'Review' }), site.selectedView === 1 && React.createElement(FormattedMessage, { defaultMessage: 'Duplicate Project' }) ) ) ); } export default DuplicateSiteDialogContainer;