diff --git a/CHANGELOG.md b/CHANGELOG.md index ddc33ee..5bd1004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,19 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). -### [2.0.9](https://github.com/eea/volto-industry-theme/compare/2.0.8...2.0.9) - 3 October 2022 +### [2.0.10](https://github.com/eea/volto-industry-theme/compare/2.0.9...2.0.10) - 6 October 2022 + +#### :rocket: New Features + +- feat: Apply siteName & facilityName filters [Miu Razvan - [`ce2a3be`](https://github.com/eea/volto-industry-theme/commit/ce2a3be2f260cf78a01ed22b229a09acdbdd2445)] #### :hammer_and_wrench: Others -- proper tap size mobile devices [andreiggr - [`7330c31`](https://github.com/eea/volto-industry-theme/commit/7330c31773c5cc2976283db204a5f96cc3bc1579)] +- Use volto 16 [Miu Razvan - [`15b6739`](https://github.com/eea/volto-industry-theme/commit/15b673915b03a43319a08a708eef89ccf728876b)] +- Clean up [Miu Razvan - [`bc98766`](https://github.com/eea/volto-industry-theme/commit/bc98766e4f1031cf021d76c47e7b4aa3af5e3a99)] +- Add FacilityName filter [Miu Razvan - [`2e719ca`](https://github.com/eea/volto-industry-theme/commit/2e719ca1346450d08cd57309cdd49ae434cff55c)] +### [2.0.9](https://github.com/eea/volto-industry-theme/compare/2.0.8...2.0.9) - 3 October 2022 + ### [2.0.8](https://github.com/eea/volto-industry-theme/compare/2.0.7...2.0.8) - 30 September 2022 #### :hammer_and_wrench: Others diff --git a/Jenkinsfile b/Jenkinsfile index 2e73741..74cd622 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -41,19 +41,19 @@ pipeline { "ES lint": { node(label: 'docker') { - sh '''docker run -i --rm --name="$BUILD_TAG-eslint" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" plone/volto-addon-ci eslint''' + sh '''docker run -i --rm --name="$BUILD_TAG-eslint" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e VOLTO="$VOLTO" plone/volto-addon-ci eslint''' } }, "Style lint": { node(label: 'docker') { - sh '''docker run -i --rm --name="$BUILD_TAG-stylelint" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" plone/volto-addon-ci stylelint''' + sh '''docker run -i --rm --name="$BUILD_TAG-stylelint" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e VOLTO="$VOLTO" plone/volto-addon-ci stylelint''' } }, "Prettier": { node(label: 'docker') { - sh '''docker run -i --rm --name="$BUILD_TAG-prettier" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" plone/volto-addon-ci prettier''' + sh '''docker run -i --rm --name="$BUILD_TAG-prettier" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e VOLTO="$VOLTO" plone/volto-addon-ci prettier''' } } ) diff --git a/package.json b/package.json index f162fff..1dd5683 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eeacms/volto-industry-theme", - "version": "2.0.9", + "version": "2.0.10", "description": "@eeacms/volto-industry-theme: Volto add-on", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", diff --git a/src/components/manage/Blocks/FiltersBlock/Search.js b/src/components/manage/Blocks/FiltersBlock/Search.js index 7433ff1..7185a1c 100644 --- a/src/components/manage/Blocks/FiltersBlock/Search.js +++ b/src/components/manage/Blocks/FiltersBlock/Search.js @@ -24,10 +24,10 @@ const getSites = (value) => { const db_version = process.env.RAZZLE_DB_VERSION || config.settings.db_version || 'latest'; const query = `SELECT siteName - FROM [IED].[${db_version}].[SiteMap] - WHERE [siteName] COLLATE Latin1_General_CI_AI LIKE '%${value}%' - GROUP BY siteName - ORDER BY [siteName]`; + FROM [IED].[${db_version}].[SiteMap] + WHERE [siteName] COLLATE Latin1_General_CI_AI LIKE '%${value}%' + GROUP BY siteName + ORDER BY [siteName]`; return axios.get( providerUrl + `?${getEncodedQueryString(query)}&p=1&nrOfHits=6`, @@ -43,11 +43,27 @@ const getLocations = (value, providers_data) => { ); }; +const getFacilities = (value) => { + const providerUrl = config.settings.providerUrl; + const db_version = + process.env.RAZZLE_DB_VERSION || config.settings.db_version || 'latest'; + const query = `SELECT facilityName + FROM [IED].[${db_version}].[ProductionFacility] + WHERE [facilityName] COLLATE Latin1_General_CI_AI LIKE '%${value}%' + GROUP BY facilityName + ORDER BY [facilityName]`; + + return axios.get( + providerUrl + `?${getEncodedQueryString(query)}&p=1&nrOfHits=6`, + ); +}; + const Search = ({ data, providers_data, query, setQuery, ...props }) => { const searchContainer = React.useRef(); const [loading, setLoading] = React.useState(false); const [showResults, setShowResults] = React.useState(false); const [sites, setSites] = React.useState([]); + const [facilities, setFacilities] = React.useState([]); const [locations, setLocations] = React.useState([]); const value = React.useMemo(() => { @@ -69,18 +85,65 @@ const Search = ({ data, providers_data, query, setQuery, ...props }) => { const items = React.useMemo(() => { if (loading) return []; - const half = MAX_RESULTS / 2; - let _locations = locations.slice(0, half); - let _sites = sites.slice(0, half); - if (_locations.length < half) { - _sites = sites.slice(0, MAX_RESULTS - _locations.length); + const slices = MAX_RESULTS / 3; + let total_entities = 1; + let empty_slices = 0; + let entities = 0; + let _locations = locations.slice(0, slices); + let _sites = sites.slice(0, slices); + let _facilities = facilities.slice(0, slices); + + if (_locations.length < slices) { + empty_slices += slices - _locations.length; + entities++; + } else if (_locations.length <= slices) { + total_entities++; } - if (_sites.length < half) { - _locations = locations.slice(0, MAX_RESULTS - _sites.length); + if (_sites.length < slices) { + empty_slices += slices - _sites.length; + entities++; + } else if (_sites.length <= slices) { + total_entities++; + } + if (_facilities.length < slices) { + empty_slices += slices - _facilities.length; + entities++; + } else if (_facilities.length <= slices) { + total_entities++; + } + + if (locations.length > slices && empty_slices > 0) { + const used_slices = Math.round( + empty_slices / (total_entities - entities), + ); + _locations = locations.slice(0, slices + used_slices); + empty_slices -= used_slices; + } else { + total_entities--; + } + + if (sites.length > slices && empty_slices > 0) { + const used_slices = Math.round( + empty_slices / (total_entities - entities), + ); + _sites = sites.slice(0, slices + used_slices); + empty_slices -= used_slices; + } else { + total_entities--; } - return [..._locations, ..._sites]; - }, [sites, locations, loading]); + if (facilities.length > slices && empty_slices > 0) { + const used_slices = Math.round( + empty_slices / (total_entities - entities), + ); + _facilities = facilities.slice(0, slices + used_slices); + empty_slices -= used_slices; + } else { + total_entities--; + } + + return [..._locations, ..._sites, ..._facilities]; + }, [sites, facilities, locations, loading]); const handleClickOutside = React.useCallback((event) => { if (!doesNodeContainClick(searchContainer.current, event)) { @@ -96,6 +159,7 @@ const Search = ({ data, providers_data, query, setQuery, ...props }) => { const requests = []; if (value.length >= 3) { requests.push(getSites(value)); + requests.push(getFacilities(value)); requests.push(getLocations(value, providers_data)); Promise.all(requests).then((responses) => { @@ -105,17 +169,24 @@ const Search = ({ data, providers_data, query, setQuery, ...props }) => { type: 'site', })) || [], ); + setFacilities( + responses[1].data?.results?.map((item) => ({ + text: item.facilityName, + type: 'facility', + })) || [], + ); setLocations( - responses[1].data?.suggestions?.map((item) => ({ + responses[2].data?.suggestions?.map((item) => ({ ...item, type: 'location', })) || [], ); + setLoading(false); }); - setLoading(false); } else { setLoading(false); setSites([]); + setFacilities([]); setLocations([]); } }, diff --git a/src/components/manage/Blocks/IndustryDataTable/View.jsx b/src/components/manage/Blocks/IndustryDataTable/View.jsx index 8114957..67ca9aa 100644 --- a/src/components/manage/Blocks/IndustryDataTable/View.jsx +++ b/src/components/manage/Blocks/IndustryDataTable/View.jsx @@ -150,9 +150,6 @@ const getQuery = (query) => { ...(isNotEmpty(query.filter_countries) ? { 'countryCode[in]': query.filter_countries } : {}), - ...(query.filter_search?.text && query.filter_search?.type === 'search-site' - ? { siteName: query.filter_search.text } - : {}), }; return obj; }; @@ -173,10 +170,32 @@ const getConditions = (query) => { query.filter_pollutant_groups.map((filter) => `%${filter}%`), ) : []), + ...(query.filter_search?.text && query.filter_change?.type === 'search-site' + ? [ + { + like: [ + 'siteName', + { literal: query.filter_search.text.replaceAll("'", "''") }, + ], + }, + ] + : []), + ...(query.filter_search?.text && + query.filter_change?.type === 'search-facility' + ? [ + { + like: [ + 'facilityNames', + { literal: query.filter_search.text.replaceAll("'", "''") }, + ], + }, + ] + : []), ]; }; const View = (props) => { + const table = React.useRef(); const context = React.useContext(ConnectorContext); const [openedRow, setOpenedRow] = React.useState(null); const { @@ -218,7 +237,7 @@ const View = (props) => { }, [JSON.stringify(query)]); return ( -
+
{row_size && tableData ? ( { { if (pagination.activePage > 1) { updatePagination({ @@ -469,7 +490,7 @@ const View = (props) => { { as="a" icon disabled={ - props.isPending || + props.loadingProviderData || pagination.activePage === pagination.lastPage } onClick={() => { diff --git a/src/components/manage/Blocks/IndustryMap/View.jsx b/src/components/manage/Blocks/IndustryMap/View.jsx index 94ddfb2..ddf39b4 100644 --- a/src/components/manage/Blocks/IndustryMap/View.jsx +++ b/src/components/manage/Blocks/IndustryMap/View.jsx @@ -25,6 +25,7 @@ import { getLayerBaseURL, getLocationExtent, getSiteExtent, + getFacilityExtent, getCountriesExtent, getWhereStatement, } from './index'; @@ -197,6 +198,31 @@ class View extends React.PureComponent { }); } }); + } else if (filter_change.type === 'search-facility') { + getFacilityExtent(filter_search).then(({ data }) => { + const extent = data?.results?.[0] || {}; + if ( + extent.MIN_X === null || + extent.MIN_Y === null || + extent.MAX_X === null || + extent.MAX_Y === null + ) { + toast.warn( + , + ); + } else { + this.map.current + .getView() + .fit([extent.MIN_X, extent.MIN_Y, extent.MAX_X, extent.MAX_Y], { + maxZoom: 16, + duration: 1000, + }); + } + }); } else if ( (filter_change.type === 'advanced-filter' || filter_change.type === 'simple-filter') && diff --git a/src/components/manage/Blocks/IndustryMap/index.js b/src/components/manage/Blocks/IndustryMap/index.js index 1618df6..3631e2c 100644 --- a/src/components/manage/Blocks/IndustryMap/index.js +++ b/src/components/manage/Blocks/IndustryMap/index.js @@ -192,13 +192,31 @@ export const getSiteExtent = (data) => { MAX(shape_wm.STX) AS MAX_X, MAX(shape_wm.STY) AS MAX_Y FROM [IED].[${db_version}].[SiteMap] - WHERE [siteName] COLLATE Latin1_General_CI_AI LIKE '%${data.text.replace( + WHERE [siteName] COLLATE Latin1_General_CI_AI LIKE '%${data.text.replaceAll( "'", "''", )}%'`)}`, ); }; +export const getFacilityExtent = (data) => { + const db_version = + process.env.RAZZLE_DB_VERSION || config.settings.db_version || 'latest'; + + let text = data.text.replaceAll('\n', ''); + text = text.replaceAll("'", "''"); + + return axios.get( + `${config.settings.providerUrl}?${getEncodedQueryString(`SELECT + MIN(shape_wm.STX) AS MIN_X, + MIN(shape_wm.STY) AS MIN_Y, + MAX(shape_wm.STX) AS MAX_X, + MAX(shape_wm.STY) AS MAX_Y + FROM [IED].[${db_version}].[SiteMap] + WHERE [facilityNames] COLLATE Latin1_General_CI_AI LIKE '%${text}%'`)}`, + ); +}; + export const getCountriesExtent = (countries) => { const requests = []; countries.forEach((country) => { @@ -313,6 +331,10 @@ export const getWhereStatement = (data) => { where[filter++] = [`siteName LIKE '${search.text}%'`]; } + if (search?.type === 'facility' && search?.text) { + where[filter++] = [`facilityNames LIKE '${search.text}%'`]; + } + return where .filter((w) => w.length) .map((w) => `(${w.join(' OR ')})`) diff --git a/src/customizations/volto/components/theme/View/DefaultView.jsx b/src/customizations/volto/components/theme/View/DefaultView.jsx new file mode 100644 index 0000000..a36f173 --- /dev/null +++ b/src/customizations/volto/components/theme/View/DefaultView.jsx @@ -0,0 +1,117 @@ +/** + * Document view component. + * @module components/theme/View/DefaultView + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl } from 'react-intl'; + +import { Container, Segment, Grid, Label } from 'semantic-ui-react'; +import config from '@plone/volto/registry'; +import { getSchema } from '@plone/volto/actions'; +import { getWidget } from '@plone/volto/helpers/Widget/utils'; +import RenderBlocks from '@plone/volto/components/theme/View/RenderBlocks'; + +import { hasBlocksData, getBaseUrl } from '@plone/volto/helpers'; +import { useDispatch, useSelector } from 'react-redux'; + +/** + * Component to display the default view. + * @function DefaultView + * @param {Object} content Content object. + * @returns {string} Markup of the component. + */ +const DefaultView = (props) => { + const { content, location } = props; + const path = getBaseUrl(location?.pathname || ''); + const dispatch = useDispatch(); + const { views } = config.widgets; + const contentSchema = useSelector((state) => state.schema?.schema); + const fieldsetsToExclude = [ + 'categorization', + 'dates', + 'ownership', + 'settings', + ]; + const fieldsets = contentSchema?.fieldsets.filter( + (fs) => !fieldsetsToExclude.includes(fs.id), + ); + + React.useEffect(() => { + content?.['@type'] && + !hasBlocksData(content) && + dispatch(getSchema(content['@type'], location.pathname)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return hasBlocksData(content) ? ( +
+ +
+ ) : ( + + {fieldsets?.map((fs) => { + return ( +
+ {fs.id !== 'default' &&

{fs.title}

} + {fs.fields?.map((f, key) => { + let field = { + ...contentSchema?.properties[f], + id: f, + widget: getWidget(f, contentSchema?.properties[f]), + }; + let Widget = views?.getWidget(field); + return f !== 'title' ? ( + + + + + + + + + + + ) : ( + + ); + })} +
+ ); + })} +
+ ); +}; + +/** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ +DefaultView.propTypes = { + /** + * Content of the object + */ + content: PropTypes.shape({ + /** + * Title of the object + */ + title: PropTypes.string, + /** + * Description of the object + */ + description: PropTypes.string, + /** + * Text of the object + */ + text: PropTypes.shape({ + /** + * Data of the text of the object + */ + data: PropTypes.string, + }), + }).isRequired, +}; + +export default injectIntl(DefaultView); diff --git a/src/customizations/volto/components/theme/View/RenderBlocks.jsx b/src/customizations/volto/components/theme/View/RenderBlocks.jsx new file mode 100644 index 0000000..148c996 --- /dev/null +++ b/src/customizations/volto/components/theme/View/RenderBlocks.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { getBaseUrl, applyBlockDefaults } from '@plone/volto/helpers'; +import { defineMessages, injectIntl } from 'react-intl'; +import { map } from 'lodash'; +import { + getBlocksFieldname, + getBlocksLayoutFieldname, + hasBlocksData, +} from '@plone/volto/helpers'; +import StyleWrapper from '@plone/volto/components/manage/Blocks/Block/StyleWrapper'; +import config from '@plone/volto/registry'; + +const messages = defineMessages({ + unknownBlock: { + id: 'Unknown Block', + defaultMessage: 'Unknown Block {block}', + }, +}); + +const RenderBlocks = (props) => { + const { content, intl, location, metadata } = props; + const blocksFieldname = getBlocksFieldname(content); + const blocksLayoutFieldname = getBlocksLayoutFieldname(content); + const blocksConfig = props.blocksConfig || config.blocks.blocksConfig; + const CustomTag = props.as || React.Fragment; + + return hasBlocksData(content) ? ( + + {map(content[blocksLayoutFieldname].items, (block) => { + const Block = + blocksConfig[content[blocksFieldname]?.[block]?.['@type']]?.view; + + const blockData = applyBlockDefaults({ + data: content[blocksFieldname][block], + intl, + metadata, + properties: content, + }); + + return Block ? ( + + + + ) : ( +
+ {intl.formatMessage(messages.unknownBlock, { + block: content[blocksFieldname]?.[block]?.['@type'], + })} +
+ ); + })} +
+ ) : ( + '' + ); +}; + +export default injectIntl(RenderBlocks);