@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
531 lines (529 loc) • 21.6 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-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;