@plone/volto
Version:
Volto
305 lines (289 loc) • 10.6 kB
JSX
import React, { useEffect, useState } from 'react';
import { Button, Container, Segment, Table } from 'semantic-ui-react';
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers/Url/Url';
import langmap from '@plone/volto/helpers/LanguageMap/LanguageMap';
import reduce from 'lodash/reduce';
import { Link, useLocation } from 'react-router-dom';
import Icon from '@plone/volto/components/theme/Icon/Icon';
import Toast from '@plone/volto/components/manage/Toast/Toast';
import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
import config from '@plone/volto/registry';
import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
import {
deleteLinkTranslation,
linkTranslation,
} from '@plone/volto/actions/translations/translations';
import { getContent } from '@plone/volto/actions/content/content';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useSelector, useDispatch } from 'react-redux';
import { createPortal } from 'react-dom';
import { toast } from 'react-toastify';
import addSVG from '@plone/volto/icons/add.svg';
import backSVG from '@plone/volto/icons/back.svg';
import linkSVG from '@plone/volto/icons/link.svg';
import unlinkSVG from '@plone/volto/icons/unlink.svg';
const messages = defineMessages({
success: {
id: 'Success',
defaultMessage: 'Success',
},
linked: {
id: 'Translation linked',
defaultMessage: 'Translation linked',
},
unlinked: {
id: 'Translation linking removed',
defaultMessage: 'Translation linking removed',
},
link: {
id: 'Link translation for',
defaultMessage: 'Link translation for',
},
unlink: {
id: 'Unlink translation for',
defaultMessage: 'Unlink translation for',
},
ManageTranslations: {
id: 'Manage Translations',
defaultMessage: 'Manage Translations',
},
ManageTranslationsTitle: {
id: 'Manage translations for {title}',
defaultMessage: 'Manage translations for {title}',
},
back: {
id: 'Back',
defaultMessage: 'Back',
},
});
const ManageTranslations = (props) => {
const intl = useIntl();
const pathname = useLocation().pathname;
const content = useSelector((state) => state.content.data);
const dispatch = useDispatch();
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
const { isObjectBrowserOpen, openObjectBrowser } = props;
const currentSelectedItem = React.useRef(null);
React.useEffect(() => {
if (!content) {
dispatch(getContent(getBaseUrl(pathname)));
}
}, [dispatch, content, pathname]);
React.useEffect(() => {
// Only execute the link API call on the final item selected, once the ObjectBrowser
// is closed
if (!isObjectBrowserOpen && currentSelectedItem.current) {
dispatch(
linkTranslation(
flattenToAppURL(content['@id']),
currentSelectedItem.current,
),
)
.then((resp) => {
toast.success(
<Toast
success
title={intl.formatMessage(messages.success)}
content={intl.formatMessage(messages.linked)}
/>,
);
dispatch(getContent(getBaseUrl(pathname)));
})
.catch((error) => {
// TODO: The true error sent by the API is shadowed by the superagent one
// Update this when this issue is fixed.
const shadowedError = JSON.parse(error.response.text);
toast.error(
<Toast
error
title={shadowedError.error.type}
content={shadowedError.error.message}
/>,
{ toastId: 'linkFailed' },
);
});
}
/* eslint-disable react-hooks/exhaustive-deps */
}, [isObjectBrowserOpen]);
const translations = content &&
content['@components'].translations.items && {
[content.language.token]: {
url: content['@id'],
},
...reduce(
content['@components'].translations.items,
(acc, value) => {
return { ...acc, [value.language]: { url: value['@id'] } };
},
{},
),
};
function onSelectTarget(target) {
// We store the selection temporarily on the component, because we don't want it to
// execute it right away, since that will lead into duplicate link requests and we
// only want the last to get through
currentSelectedItem.current = target;
}
function onDeleteTranslation(lang) {
dispatch(deleteLinkTranslation(flattenToAppURL(content['@id']), lang))
.then((resp) => {
toast.success(
<Toast
success
title={intl.formatMessage(messages.success)}
content={intl.formatMessage(messages.unlinked)}
/>,
);
dispatch(getContent(getBaseUrl(pathname)));
})
.catch((error) => {
// TODO: The true error sent by the API is shadowed by the superagent one
// Update this when this issue is fixed.
const shadowedError = JSON.parse(error.response.text);
toast.error(
<Toast
error
title={shadowedError.error.type}
content={shadowedError.error.message}
/>,
{ toastId: 'linkFailed' },
);
});
}
return (
<Container id="page-manage-translations">
<Helmet title={intl.formatMessage(messages.ManageTranslations)} />
<Segment.Group raised>
<Segment className="primary">
<FormattedMessage
id="Manage translations for {title}"
defaultMessage="Manage translations for {title}"
values={{ title: <q>{content.title}</q> }}
/>
</Segment>
{content && (
<Table selectable compact singleLine attached>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Language</Table.HeaderCell>
<Table.HeaderCell>Path</Table.HeaderCell>
<Table.HeaderCell textAlign="right">Tools</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{config.settings.supportedLanguages.map((lang) => (
<Table.Row key={lang}>
<Table.Cell collapsing>
{lang === content.language.token ? (
<strong>{langmap[lang].nativeName}</strong>
) : (
langmap[lang].nativeName
)}
</Table.Cell>
<Table.Cell>
<Link to={flattenToAppURL(translations[lang]?.url || '')}>
{flattenToAppURL(translations[lang]?.url || '')}
</Link>
</Table.Cell>
<Table.Cell
textAlign="right"
className="manage-multilingual-tools"
>
<Button.Group>
<Button
basic
icon
disabled={
lang === content.language.token ||
translations?.[lang]
}
as={Link}
to={{
pathname: `${pathname}/create-translation`,
state: {
type: content['@type'],
translationOf: flattenToAppURL(content['@id']),
language: lang,
},
}}
>
<Icon name={addSVG} size="24px" />
</Button>
</Button.Group>
{translations?.[lang] ? (
<Button.Group>
<Button
aria-label={`${intl.formatMessage(
messages.unlink,
)} ${langmap[lang].nativeName.toLowerCase()}`}
basic
icon
disabled={lang === content.language.token}
onClick={() => onDeleteTranslation(lang)}
>
<Icon
name={
lang === content.language.token
? linkSVG
: unlinkSVG
}
size="24px"
/>
</Button>
</Button.Group>
) : (
<Button.Group>
<Button
aria-label={`${intl.formatMessage(
messages.link,
)} ${langmap[lang].nativeName.toLowerCase()}`}
basic
icon
disabled={lang === content.language.token}
onClick={() =>
openObjectBrowser({
mode: 'link',
overlay: true,
onSelectItem: (url) => {
onSelectTarget(url, isObjectBrowserOpen);
},
})
}
>
<Icon name={linkSVG} size="24px" />
</Button>
</Button.Group>
)}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
)}
{isClient &&
createPortal(
<Toolbar
pathname={pathname}
hideDefaultViewButtons
inner={
<Link to={`${getBaseUrl(pathname)}`} className="item">
<Icon
name={backSVG}
className="contents circled"
size="30px"
title={intl.formatMessage(messages.back)}
/>
</Link>
}
/>,
document.getElementById('toolbar'),
)}
</Segment.Group>
</Container>
);
};
export default withObjectBrowser(ManageTranslations);