Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] Allow for queries/filters on collections when generating the sitemap #109

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
This is a fork of the work done by the original author. This fork includes an additional feature allowing you to add a conditional to any url bundle. For example you may have a published draft of an entry that is temporarily disabled with a flag, is not available in a certain locale, or maybe you don't want a search engine to index a page without any proper SEO content.

<div align="center">
<h1>Strapi sitemap plugin</h1>

Expand Down
103 changes: 103 additions & 0 deletions admin/src/components/ModalForm/Collection/Filters/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import * as React from 'react';

import {
Grid,
Typography,
Flex,
Button,
} from '@strapi/design-system';
import { useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';

import SelectConditional from '../../../SelectConditional';
import { onChangeContentTypes } from '../../../../state/actions/Sitemap';

// eslint-disable-next-line arrow-body-style
const Filters = (props) => {
// eslint-disable-next-line @typescript-eslint/unbound-method
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const [conditionCount, setConditionCount] = React.useState(0);
const {
modifiedState,
uid,
langcode,
contentTypes,
} = props;

React.useEffect(() => {
// get the initial condition count
const count = Object.keys(modifiedState.getIn([uid, 'filters'], []).toJS()).length;
setConditionCount(count);
}, [uid, langcode]);

const handleRemoveCondition = () => {
dispatch(onChangeContentTypes(uid, null, ['filters', conditionCount, 'field'], ''));
dispatch(onChangeContentTypes(uid, null, ['filters', conditionCount, 'operator'], ''));
dispatch(onChangeContentTypes(uid, null, ['filters', conditionCount, 'value'], ''));
setConditionCount(conditionCount - 1);
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleRemoveCondition function always removes the last filter. It would be neat if we can specify the filter we want to remove. Giving the end user more control on the filters interface.

return (
<div>
{conditionCount === 0 ? (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this condition can be optimized.

The only thing that should/shouldn't show for this ternary is the list of conditions. Though this ternary is used to render the entire component leaving duplicate code below (e.g. the add/remove buttons).

If we use the ternary just for the conditions list this would be resolved.

<Flex direction="column" alignItems="stretch">
<Flex direction="row" justifyContent="space-between">
<Flex direction="column" justifyContent="center" alignItems="flex-start">
<Typography variant="pi" fontWeight="bold">
{formatMessage({ id: 'sitemap.Settings.Field.Condition.Label', defaultMessage: 'Conditional Filtering' })}
</Typography>
<Typography>
{formatMessage({ id: 'sitemap.Settings.Field.Condition.Description', defaultMessage: 'Only include URLs that match the following condition.' })}
</Typography>
</Flex>
<Flex direction="row" alignItems="center" gap={2}>
<Button onClick={() => setConditionCount(conditionCount + 1)}>
{formatMessage({ id: 'sitemap.Settings.Field.Condition.Add', defaultMessage: 'Add' })}
</Button>
<Button variant="secondary" onClick={() => handleRemoveCondition()}>
{formatMessage({ id: 'sitemap.Settings.Field.Condition.Remove', defaultMessage: 'Remove' })}
</Button>
</Flex>
</Flex>
</Flex>
) : (
<Flex direction="column" alignItems="stretch">
<Flex direction="row" justifyContent="space-between" style={{ marginBottom: '2rem' }}>
<Flex direction="column" justifyContent="center" alignItems="flex-start">
<Typography variant="pi" fontWeight="bold">
{formatMessage({ id: 'sitemap.Settings.Field.Condition.Label', defaultMessage: 'Conditional Filtering' })}
</Typography>
<Typography>
{formatMessage({ id: 'sitemap.Settings.Field.Condition.Description', defaultMessage: 'Only include URLs that match the following conditions.' })}
</Typography>
</Flex>
<Flex direction="row" alignItems="center" gap={2}>
<Button onClick={() => setConditionCount(conditionCount + 1)}>
{formatMessage({ id: 'sitemap.Settings.Field.Condition.Add', defaultMessage: 'Add' })}
</Button>
<Button variant="secondary" onClick={() => handleRemoveCondition()}>
{formatMessage({ id: 'sitemap.Settings.Field.Condition.Remove', defaultMessage: 'Remove' })}
</Button>
</Flex>
</Flex>
{Array.from(Array(conditionCount).keys()).map((i) => (
<Grid gap={4} style={{ marginBottom: '1rem' }}>
<SelectConditional
disabled={!uid || (contentTypes[uid].locales && !langcode)}
contentType={contentTypes[uid]}
onConditionChange={(value) => dispatch(onChangeContentTypes(uid, null, ['filters', String(i), 'field'], value))}
onOperatorChange={(value) => dispatch(onChangeContentTypes(uid, null, ['filters', String(i), 'operator'], value))}
onValueChange={(value) => dispatch(onChangeContentTypes(uid, null, ['filters', String(i), 'value'], value))}
condition={modifiedState.getIn([uid, 'filters', String(i), 'field'], '')}
conditionOperator={modifiedState.getIn([uid, 'filters', String(i), 'operator'], '')}
conditionValue={modifiedState.getIn([uid, 'filters', String(i), 'value'], '')}
/>
</Grid>
))}
</Flex>
)}
</div>
);
};

export default Filters;
34 changes: 13 additions & 21 deletions admin/src/components/ModalForm/index.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useIntl } from 'react-intl';

import { request, InjectionZone } from '@strapi/helper-plugin';

import { useSelector } from 'react-redux';
import { request } from '@strapi/helper-plugin';

import {
ModalLayout,
Expand All @@ -24,16 +22,14 @@ import {

import CustomForm from './Custom';
import CollectionForm from './Collection';
import pluginId from '../../helpers/pluginId';
import Filters from './Collection/Filters';

const ModalForm = (props) => {
const [uid, setUid] = useState('');
const [langcode, setLangcode] = useState('und');
const [patternInvalid, setPatternInvalid] = useState({ invalid: false });
const { formatMessage } = useIntl();

const hasPro = useSelector((state) => state.getIn(['sitemap', 'info', 'hasPro'], false));

const {
onSubmit,
onCancel,
Expand Down Expand Up @@ -104,27 +100,23 @@ const ModalForm = (props) => {
</ModalHeader>
<ModalBody>
<TabGroup label="Settings" id="tabs" variant="simple">
{hasPro && (
<Box marginBottom="4">
<Flex>
<Tabs style={{ marginLeft: 'auto' }}>
<Tab>{formatMessage({ id: 'sitemap.Modal.Tabs.Basic.Title', defaultMessage: 'Basic settings' })}</Tab>
<Tab>{formatMessage({ id: 'sitemap.Modal.Tabs.Advanced.Title', defaultMessage: 'Advanced settings' })}</Tab>
</Tabs>
</Flex>

<Divider />
</Box>
)}
<Box marginBottom="4">
<Flex>
<Tabs style={{ marginLeft: 'auto' }}>
<Tab>{formatMessage({ id: 'sitemap.Modal.Tabs.Basic.Title', defaultMessage: 'Basic settings' })}</Tab>
<Tab>{formatMessage({ id: 'sitemap.Modal.Tabs.Filters.Title', defaultMessage: 'Filters' })}</Tab>
</Tabs>
</Flex>

<Divider />
</Box>

<TabPanels>
<TabPanel>
{form()}
</TabPanel>
<TabPanel>
<InjectionZone
area={`${pluginId}.modal.advanced`}
/>
<Filters uid={uid} langcode={langcode} {...props} />
</TabPanel>
</TabPanels>
</TabGroup>
Expand Down
85 changes: 85 additions & 0 deletions admin/src/components/SelectConditional/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import { Select, Option, TextInput, GridItem } from '@strapi/design-system';
import { useIntl } from 'react-intl';

const operators = [
'$not',
'$eq',
'$eqi',
'$ne',
'$in',
'$notIn',
'$lt',
'$lte',
'$gt',
'$gte',
'$between',
'$contains',
'$notContains',
'$containsi',
'$notContainsi',
'$startsWith',
'$endsWith',
'$null',
'$notNull',
];

const SelectConditional = (props) => {
const { formatMessage } = useIntl();

const {
contentType,
disabled,
onConditionChange,
onOperatorChange,
onValueChange,
condition,
conditionOperator,
conditionValue,
} = props;

return (
<>
<GridItem col={4}>
<Select
name="select"
label={formatMessage({ id: 'sitemap.Settings.Field.SelectConditional.Label', defaultMessage: 'Attribute' })}
hint={formatMessage({ id: 'sitemap.Settings.Field.SelectConditional.Description', defaultMessage: 'Select an attribute' })}
disabled={disabled}
onChange={(condition) => onConditionChange(condition)}
value={condition}
>
{contentType && contentType.attributes.map((attribute) => {
return <Option value={attribute} key={attribute}>{attribute}</Option>;
})}
</Select>
</GridItem>
<GridItem col={2}>
<Select
name="select"
label={formatMessage({ id: 'sitemap.Settings.Field.SelectOperator.Label', defaultMessage: 'Operator' })}
hint={formatMessage({ id: 'sitemap.Settings.Field.SelectOperator.Description', defaultMessage: 'Select an operator' })}
disabled={disabled}
onChange={(operator) => onOperatorChange(operator)}
value={conditionOperator}
>
{operators.map((operator) => {
return <Option value={operator} key={operator}>{operator}</Option>;
})}
</Select>
</GridItem>
<GridItem col={6}>
<TextInput
disabled={disabled}
label={formatMessage({ id: 'sitemap.Settings.Field.SelectConditionValue.Label', defaultMessage: 'Value' })}
name="conditionValue"
hint={formatMessage({ id: 'sitemap.Settings.Field.SelectConditionValue.Description', defaultMessage: '"Text", true, 2, etc' })}
onChange={(e) => onValueChange(e.target.value)}
value={conditionValue}
/>
</GridItem>
</>
);
};

export default SelectConditional;
4 changes: 3 additions & 1 deletion admin/src/state/reducers/Sitemap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { fromJS, Map } from 'immutable';
import { isArray } from 'lodash';

import {
GET_SETTINGS_SUCCEEDED,
Expand Down Expand Up @@ -59,8 +60,9 @@ export default function sitemapReducer(state = initialState, action) {
return state
.updateIn(['modifiedContentTypes', action.contentType, 'languages', action.lang, action.key], () => action.value);
} else {
const keys = isArray(action.key) ? action.key : Array(action.key);
return state
.updateIn(['modifiedContentTypes', action.contentType, action.key], () => action.value);
.updateIn(['modifiedContentTypes', action.contentType, ...keys], () => action.value);
}
case ON_CHANGE_CUSTOM_ENTRY:
return state
Expand Down
8 changes: 8 additions & 0 deletions server/controllers/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ module.exports = {
if (strapi.config.get('plugin.sitemap.excludedTypes').includes(contentType.uid)) return;
contentTypes[contentType.uid] = {
displayName: contentType.globalId,
attributes: Object.keys(contentType.attributes).filter((key) => {
if (key === 'id' || key === 'created_at' || key === 'updated_at') return false;
if (contentType.attributes[key].type === 'component') return false;
if (contentType.attributes[key].type === 'dynamiczone') return false;
if (contentType.attributes[key].type === 'relation') return false;
if (contentType.attributes[key].private === true) return false;
return true;
}) ?? [],
};

if (strapi.plugin('i18n') && _.get(contentType, 'pluginOptions.i18n.localized')) {
Expand Down
13 changes: 12 additions & 1 deletion server/services/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,19 @@ const createSitemapEntries = async (invalidationObject) => {

cacheEntries[contentType] = {};

const filters = config.contentTypes[contentType]['filters'];
const formattedFilters = {};
for (let i = 0; i <= Object.keys(filters).length; i++) {
let exists = filters[i]?.field && filters[i]?.operator && filters[i]?.value;
if (exists) {
formattedFilters[filters[i].field] = {
[filters[i].operator]: filters[i].value,
};
}
}

// Query all the pages
const pages = await getService('query').getPages(config, contentType, invalidationObject?.[contentType]?.ids);
const pages = await getService('query').getPages(config, contentType, invalidationObject?.[contentType]?.ids, formattedFilters);

// Add formatted sitemap page data to the array.
await Promise.all(pages.map(async (page, i) => {
Expand Down
4 changes: 3 additions & 1 deletion server/services/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,11 @@ const getRelationsFromConfig = (contentType) => {
* @param {obj} config - The config object
* @param {string} contentType - Query only entities of this type.
* @param {array} ids - Query only these ids.
* @param {object} filters - Custom filters
*
* @returns {object} The pages.
*/
const getPages = async (config, contentType, ids) => {
const getPages = async (config, contentType, ids, filters = {}) => {
const excludeDrafts = config.excludeDrafts && strapi.contentTypes[contentType].options.draftAndPublish;
const isLocalized = strapi.contentTypes[contentType].pluginOptions?.i18n?.localized;

Expand All @@ -105,6 +106,7 @@ const getPages = async (config, contentType, ids) => {
id: ids ? {
$in: ids,
} : {},
...filters,
},
locale: 'all',
fields,
Expand Down
Loading