UNPKG

@plone/volto

Version:
534 lines (513 loc) 20.3 kB
import React, { useEffect, useState } from 'react'; import capitalize from 'lodash/capitalize'; import find from 'lodash/find'; import { compose } from 'redux'; import { useSelector, useDispatch } from 'react-redux'; import { FormattedMessage, useIntl } from 'react-intl'; import { toast } from 'react-toastify'; import { Button, Divider, Dropdown, Form, Header, Input, Popup, Tab, } from 'semantic-ui-react'; import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser'; import { messages } from '@plone/volto/helpers/MessageLabels/MessageLabels'; import Icon from '@plone/volto/components/theme/Icon/Icon'; import Toast from '@plone/volto/components/manage/Toast/Toast'; import { getRelationStats, queryRelations, } from '@plone/volto/actions/relations/relations'; import { rebuildRelations } from '@plone/volto/actions/relations/rebuild'; import RelationsListing from '@plone/volto/components/manage/Controlpanels/Relations/RelationsListing'; import BrokenRelations from '@plone/volto/components/manage/Controlpanels/Relations/BrokenRelations'; import helpSVG from '@plone/volto/icons/help.svg'; import clearSVG from '@plone/volto/icons/clear.svg'; import navTreeSVG from '@plone/volto/icons/nav.svg'; const RelationsMatrix = (props) => { const intl = useIntl(); const dispatch = useDispatch(); const [query_source, setQuery_source] = useState(''); const [query_target, setQuery_target] = useState(''); const [potential_targets_path, setPotential_targets_path] = useState(''); const [potential_sources_path, setPotential_sources_path] = useState(''); const [relationtype, setRelationtype] = useState(undefined); const actions = useSelector((state) => state.actions?.actions ?? {}); const can_fix_relations = find(actions.user, { id: 'plone_setup', }); const relationtypes = useSelector( (state) => state.relations?.stats?.data?.stats, ); const relationsListError = useSelector( (state) => state.relations?.stats?.error?.response?.body?.error, ); const brokenRelations = useSelector( (state) => state.relations?.stats?.data?.broken, ); let filter_options = useSelector((state) => state.groups.filter_groups); if (filter_options) { filter_options = filter_options.map((group) => ({ value: group.id, label: group.title || group.id, })); filter_options.sort(function (a, b) { var labelA = a.label.toUpperCase(); var labelB = b.label.toUpperCase(); if (labelA < labelB) { return -1; } if (labelA > labelB) { return 1; } return 0; }); } useEffect(() => { dispatch(getRelationStats()); }, [dispatch]); const onReset = (event) => { let element = event.target.querySelector('input'); element.value = ''; element.focus(); let searchtype = element.name; switch (searchtype) { case 'SearchY': setQuery_source(''); break; case 'SearchX': setQuery_target(''); break; case 'showPotentialTargets': setPotential_targets_path('/'); break; case 'showPotentialSources': setPotential_sources_path('/'); break; default: break; } }; // search for sources const onChangeSearchYs = (event) => { if (event.target.value.length > 1) { setQuery_source(event.target.value); } else { setQuery_source(''); } }; // search for targets const onChangeSearchXs = (event) => { if (event.target.value.length > 1) { setQuery_target(event.target.value); } else { setQuery_source(''); } }; const onChangeRelation = (event, { value }) => { setRelationtype(value); }; const onChangeShowPotentialSources = (_value) => { let newValue = _value; setPotential_sources_path(newValue); }; const onChangeShowPotentialTargets = (_value) => { let newValue = _value; setPotential_targets_path(newValue); }; const rebuildRelationsHandler = (flush = false) => { dispatch(rebuildRelations(flush)) .then(() => { dispatch(getRelationStats()); dispatch(queryRelations(null, true, 'broken')); }) .then(() => { toast.success( <Toast success title={intl.formatMessage(messages.success)} content="Relations updated" />, ); }) .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} />, ); }); }; const clear_potential_sources_path = () => { setPotential_sources_path(''); // onChange(id, undefined); }; const clear_potential_targets_path = () => { setPotential_targets_path(''); // onChange(id, undefined); }; const panes = [ { menuItem: intl.formatMessage(messages.inspectRelations), pane: ( <Tab.Pane attached={true} key="fix"> {relationtypes ? ( <div className="controlpanel_matrix"> <div className="controlpanel_select_relation"> <Divider hidden /> <Form className="select_relation"> <Form.Field> <Header as="h3"> <Header.Content> <FormattedMessage id="Relation name" defaultMessage="Relation" />{' '} <Dropdown placeholder={ relationtype || intl.formatMessage(messages.selectRelation) } > <Dropdown.Menu> {Object.keys(relationtypes).map((relationtype) => ( <Dropdown.Item onClick={onChangeRelation} value={relationtype} className={`select-relation-${relationtype}`} key={relationtype} > {`${relationtype} (${relationtypes[relationtype]})`} </Dropdown.Item> ))} </Dropdown.Menu> </Dropdown> </Header.Content> </Header> </Form.Field> </Form> </div> {relationtype ? ( <> <div className="controlpanel_search_wrapper"> <div className="controlpanel_search_y"> <Header as="h4"> <Header.Content> <FormattedMessage id="Source" defaultMessage="Source" /> </Header.Content> </Header> <Form className="search_y" onSubmit={onReset}> <Form.Field> <Input name="SearchY" placeholder={intl.formatMessage( messages.searchRelationSource, )} onChange={onChangeSearchYs} id="y-search-input" /> <Button.Group> <Button basic className="cancel" aria-label="cancel" onClick={(e) => { e.preventDefault(); e.stopPropagation(); document.querySelector( 'input[name="SearchY"]', ).value = ''; setQuery_source(''); }} > <Icon name={clearSVG} size="24px" /> </Button> </Button.Group> </Form.Field> </Form> <Form className="add_potential_sources" onSubmit={onReset} > <Form.Field> <Input name="showPotentialSources" type="url" value={potential_sources_path} placeholder={intl.formatMessage( messages.addPotentialSourcesPath, )} onChange={({ target }) => onChangeShowPotentialSources(target.value) } id="potential-sources-path-input" /> {potential_sources_path?.length > 0 ? ( <Button.Group> <Button basic className="cancel" aria-label="clearUrlBrowser" onClick={(e) => { e.preventDefault(); e.stopPropagation(); clear_potential_sources_path(); }} > <Icon name={clearSVG} size="24px" /> </Button> </Button.Group> ) : ( <Button.Group> <Button basic icon aria-label="openUrlBrowser" onClick={(e) => { e.preventDefault(); e.stopPropagation(); props.openObjectBrowser({ mode: 'link', overlay: true, onSelectItem: (url) => { onChangeShowPotentialSources(url); }, }); }} > <Icon name={navTreeSVG} size="24px" /> </Button> </Button.Group> )} </Form.Field> <FormattedMessage id="Show potential sources. Not only objects that are source of some relation." defaultMessage="Show potential sources. Not only objects that are source of some relation." /> </Form> </div> <div className="controlpanel_search_x"> <Form className="search_x" onSubmit={onReset}> <Header as="h4"> <Header.Content> <FormattedMessage id="Target" defaultMessage="Target" /> </Header.Content> </Header> <Form.Field> <Input name="SearchX" placeholder={intl.formatMessage( messages.searchRelationTarget, )} onChange={onChangeSearchXs} id="x-search-input" /> <Button.Group> <Button basic className="cancel" aria-label="cancel" onClick={(e) => { e.preventDefault(); e.stopPropagation(); document.querySelector( 'input[name="SearchX"]', ).value = ''; setQuery_target(''); }} > <Icon name={clearSVG} size="24px" /> </Button> </Button.Group> </Form.Field> </Form> <Form className="add_potential_targets" onSubmit={onReset} > <Form.Field> <Input name="showPotentialTargets" type="url" value={potential_targets_path} placeholder={intl.formatMessage( messages.addPotentialTargetsPath, )} onChange={({ target }) => onChangeShowPotentialTargets(target.value) } id="potential-targets-path-input" /> {potential_targets_path?.length > 0 ? ( <Button.Group> <Button basic className="cancel" aria-label="clearUrlBrowser" onClick={(e) => { e.preventDefault(); e.stopPropagation(); clear_potential_targets_path(); }} > <Icon name={clearSVG} size="24px" /> </Button> </Button.Group> ) : ( <Button.Group> <Button basic icon aria-label="openUrlBrowser" onClick={(e) => { e.preventDefault(); e.stopPropagation(); props.openObjectBrowser({ mode: 'link', overlay: true, onSelectItem: (url) => { onChangeShowPotentialTargets(url); }, }); }} > <Icon name={navTreeSVG} size="24px" /> </Button> </Button.Group> )} </Form.Field> <div className="foo"> <FormattedMessage id="Show potential targets. Not only objects that are target of some relation." defaultMessage="Show potential targets. Not only objects that are target of some relation." />{' '} <Popup trigger={ <a href="https://6.docs.plone.org/volto/development/widget.html#widget-relation-field-label" target="_blank" rel="noopener noreferrer" > <Icon name={helpSVG} size="16px" /> </a> } > <Popup.Header>Respect constraints</Popup.Header> <Popup.Content> <div> See docs.plone.org on how to respect constraints. </div> </Popup.Content> </Popup> </div> </Form> </div> </div> <div className="controlpanel_listing_wrapper"> <RelationsListing relationtype={relationtype} query_source={query_source} query_target={query_target} potential_targets_path={potential_targets_path} potential_sources_path={potential_sources_path} /> </div> </> ) : null} </div> ) : ( <p> <b>{relationsListError?.type}</b> {relationsListError?.message} </p> )} </Tab.Pane> ), }, { menuItem: intl.formatMessage(messages.fixRelations), pane: ( <Tab.Pane attached={true} key="rebuild"> <div> {!(brokenRelations && Object.keys(brokenRelations).length > 0) && ( <div> <FormattedMessage id="No broken relations found." defaultMessage="No broken relations found." /> </div> )} {can_fix_relations ? ( <React.Fragment> <Divider hidden /> <h2> {capitalize(intl.formatMessage(messages.rebuildRelations))} </h2> <Button.Group> <Button primary onClick={() => rebuildRelationsHandler(false)} content={intl.formatMessage(messages.rebuildRelations)} aria-label={intl.formatMessage(messages.rebuildRelations)} /> </Button.Group> <Divider hidden /> <h2> {capitalize( intl.formatMessage(messages.flushAndRebuildRelations), )} </h2> <div dangerouslySetInnerHTML={{ __html: intl.formatMessage( messages.flushAndRebuildRelationsHints, ), }} /> <Divider hidden /> <Button.Group> <Button secondary color="red" onClick={() => rebuildRelationsHandler(true)} content={intl.formatMessage( messages.flushAndRebuildRelations, )} aria-label={intl.formatMessage( messages.flushAndRebuildRelations, )} /> </Button.Group> </React.Fragment> ) : null} <BrokenRelations /> </div> </Tab.Pane> ), }, ]; return ( <Tab panes={panes} renderActiveOnly={false} menu={{ secondary: true, pointing: true, attached: true, tabular: true }} /> ); }; export default compose(withObjectBrowser)(RelationsMatrix);