decap-cms-core
Version:
Decap CMS core application, see decap-cms package for the main distribution.
214 lines (183 loc) • 5.06 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-polyglot';
import styled from '@emotion/styled';
import yaml from 'yaml';
import truncate from 'lodash/truncate';
import { localForage } from 'decap-cms-lib-util';
import { buttons, colors } from 'decap-cms-ui-default';
const ISSUE_URL = 'https://github.com/decaporg/decap-cms/issues/new';
function getIssueTemplate({ version, provider, browser, config }) {
return `
**Describe the bug**
**To Reproduce**
**Expected behavior**
**Screenshots**
**Applicable Versions:**
- Decap CMS version: \`${version}\`
- Git provider: \`${provider}\`
- Browser version: \`${browser}\`
**CMS configuration**
\`\`\`
${config}
\`\`\`
**Additional context**
`;
}
function buildIssueTemplate({ config }) {
let version = '';
if (typeof DECAP_CMS_VERSION === 'string') {
version = `decap-cms@${DECAP_CMS_VERSION}`;
} else if (typeof DECAP_CMS_APP_VERSION === 'string') {
version = `decap-cms-app@${DECAP_CMS_APP_VERSION}`;
}
const template = getIssueTemplate({
version,
provider: config.backend.name,
browser: navigator.userAgent,
config: yaml.stringify(config),
});
return template;
}
function buildIssueUrl({ title, config }) {
const issueUrl = config?.issue_reports?.url ?? ISSUE_URL;
try {
const body = buildIssueTemplate({ config });
const params = new URLSearchParams();
params.append('title', truncate(title, { length: 100 }));
params.append('body', truncate(body, { length: 4000, omission: '\n...' }));
params.append('labels', 'type: bug');
return `${issueUrl}?${params.toString()}`;
} catch (e) {
console.log(e);
return `${issueUrl}?template=bug_report.md`;
}
}
const ErrorBoundaryContainer = styled.div`
padding: 40px;
h1 {
font-size: 28px;
color: ${colors.text};
}
h2 {
font-size: 20px;
}
strong {
color: ${colors.textLead};
font-weight: 500;
}
hr {
width: 200px;
margin: 30px 0;
border: 0;
height: 1px;
background-color: ${colors.text};
}
a {
color: ${colors.active};
}
`;
const PrivacyWarning = styled.span`
color: ${colors.text};
`;
const CopyButton = styled.button`
${buttons.button};
${buttons.default};
${buttons.gray};
display: block;
margin: 12px 0;
`;
function RecoveredEntry({ entry, t }) {
console.log(entry);
return (
<>
<hr />
<h2>{t('ui.errorBoundary.recoveredEntry.heading')}</h2>
<strong>{t('ui.errorBoundary.recoveredEntry.warning')}</strong>
<CopyButton onClick={() => navigator.clipboard.writeText(entry)}>
{t('ui.errorBoundary.recoveredEntry.copyButtonLabel')}
</CopyButton>
<pre>
<code>{entry}</code>
</pre>
</>
);
}
export class ErrorBoundary extends React.Component {
static propTypes = {
children: PropTypes.node,
t: PropTypes.func.isRequired,
config: PropTypes.object.isRequired,
};
state = {
hasError: false,
errorMessage: '',
errorTitle: '',
backup: '',
};
static getDerivedStateFromError(error) {
console.error(error);
return {
hasError: true,
errorMessage: error.stack || error.toString(),
errorTitle: error.toString(),
};
}
componentDidMount() {
// Manually validate PropTypes - React 19 breaking change
PropTypes.checkPropTypes(ErrorBoundary.propTypes, this.props, 'prop', 'ErrorBoundary');
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.showBackup) {
return (
this.state.errorMessage !== nextState.errorMessage || this.state.backup !== nextState.backup
);
}
return true;
}
async componentDidUpdate() {
if (this.props.showBackup) {
const backup = await localForage.getItem('backup');
backup && console.log(backup);
this.setState({ backup });
}
}
render() {
const { hasError, errorMessage, backup, errorTitle } = this.state;
const { showBackup, t } = this.props;
if (!hasError) {
return this.props.children;
}
return (
<ErrorBoundaryContainer>
<h1>{t('ui.errorBoundary.title')}</h1>
<p>
<span>{t('ui.errorBoundary.details')}</span>
<a
href={buildIssueUrl({ title: errorTitle, config: this.props.config })}
target="_blank"
rel="noopener noreferrer"
data-testid="issue-url"
>
{t('ui.errorBoundary.reportIt')}
</a>
</p>
<p>
{t('ui.errorBoundary.privacyWarning')
.split('\n')
.map((item, index) => (
<>
<PrivacyWarning key={index}>{item}</PrivacyWarning>
<br />
</>
))}
</p>
<hr />
<h2>{t('ui.errorBoundary.detailsHeading')}</h2>
<p>{errorMessage}</p>
{backup && showBackup && <RecoveredEntry entry={backup} t={t} />}
</ErrorBoundaryContainer>
);
}
}
export default translate()(ErrorBoundary);