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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified .editorconfig
100644 → 100755
Empty file.
Empty file modified .eslintignore
100644 → 100755
Empty file.
Empty file modified .eslintrc
100644 → 100755
Empty file.
Empty file modified .gitattributes
100644 → 100755
Empty file.
Empty file modified .github/ISSUE_TEMPLATE/bug_report.md
100644 → 100755
Empty file.
Empty file modified .github/ISSUE_TEMPLATE/feature_request.md
100644 → 100755
Empty file.
Empty file modified .github/PULL_REQUEST_TEMPLATE.md
100644 → 100755
Empty file.
Empty file modified .github/workflows/tests.yml
100644 → 100755
Empty file.
Empty file modified .gitignore
100644 → 100755
Empty file.
Empty file modified .yamllint.yml
100644 → 100755
Empty file.
Empty file modified CODE_OF_CONDUCT.md
100644 → 100755
Empty file.
Empty file modified CONTRIBUTING.md
100644 → 100755
Empty file.
Empty file modified LICENSE.md
100644 → 100755
Empty file.
2 changes: 2 additions & 0 deletions README.md
100644 → 100755
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
Empty file modified admin/src/assets/images/logo.svg
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified admin/src/components/CMEditViewExclude/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/components/Header/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/components/HostnameModal/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/components/Info/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/components/List/Collection/Row.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/components/List/Collection/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/components/List/Custom/Row.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/components/List/Custom/index.jsx
100644 → 100755
Empty file.
105 changes: 105 additions & 0 deletions admin/src/components/ModalForm/Collection/Filters/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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;

console.log(modifiedState);

React.useEffect(() => {
// get the initial condition count
const count = Object.keys(modifiedState.getIn([uid, 'filters'], [])).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;
Empty file modified admin/src/components/ModalForm/Custom/index.jsx
100644 → 100755
Empty file.
34 changes: 13 additions & 21 deletions admin/src/components/ModalForm/index.jsx
100644 → 100755
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
Empty file modified admin/src/components/ModalForm/mapper.js
100644 → 100755
Empty file.
Empty file modified admin/src/components/PluginIcon/index.jsx
100644 → 100755
Empty file.
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;
Empty file modified admin/src/components/SelectContentTypes/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/components/SelectLanguage/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/components/Tabs/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/config/constants.js
100644 → 100755
Empty file.
Empty file modified admin/src/containers/App/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/containers/Main/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/helpers/getTrad.js
100644 → 100755
Empty file.
Empty file modified admin/src/helpers/pluginId.js
100644 → 100755
Empty file.
Empty file modified admin/src/helpers/timeFormat.js
100644 → 100755
Empty file.
Empty file modified admin/src/helpers/useActiveElement.js
100644 → 100755
Empty file.
Empty file modified admin/src/index.js
100644 → 100755
Empty file.
Empty file modified admin/src/permissions.js
100644 → 100755
Empty file.
Empty file modified admin/src/state/actions/Sitemap.js
100644 → 100755
Empty file.
4 changes: 3 additions & 1 deletion admin/src/state/reducers/Sitemap/index.js
100644 → 100755
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
Empty file modified admin/src/state/reducers/index.js
100644 → 100755
Empty file.
Empty file modified admin/src/tabs/CollectionURLs/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/tabs/CustomURLs/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/tabs/Settings/index.jsx
100644 → 100755
Empty file.
Empty file modified admin/src/translations/ar.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/de.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/en.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/es.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/fr.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/index.js
100644 → 100755
Empty file.
Empty file modified admin/src/translations/it.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/ja.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/ko.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/nl.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/pl.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/pt-BR.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/pt.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/ru.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/tr.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/vi.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/zh-Hans.json
100644 → 100755
Empty file.
Empty file modified admin/src/translations/zh.json
100644 → 100755
Empty file.
Empty file modified codecov.yml
100644 → 100755
Empty file.
Empty file modified jest.config.js
100644 → 100755
Empty file.
Empty file modified package.json
100644 → 100755
Empty file.
3 changes: 3 additions & 0 deletions public/.gitignore.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/*
/*/
!.gitignore
Empty file added public/sitemap/index.xml
Empty file.
Empty file modified server/bootstrap.js
100644 → 100755
Empty file.
Empty file modified server/config.js
100644 → 100755
Empty file.
8 changes: 8 additions & 0 deletions server/controllers/core.js
100644 → 100755
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
Empty file modified server/controllers/index.js
100644 → 100755
Empty file.
Empty file modified server/controllers/pattern.js
100644 → 100755
Empty file.
Empty file modified server/controllers/settings.js
100644 → 100755
Empty file.
5 changes: 5 additions & 0 deletions server/destroy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports = () => {
// destroy phase
};
Empty file modified server/register.js
100644 → 100755
Empty file.
Empty file modified server/routes/admin.js
100644 → 100755
Empty file.
Empty file modified server/routes/index.js
100644 → 100755
Empty file.
Empty file modified server/services/__tests__/pattern.test.js
100644 → 100755
Empty file.
13 changes: 12 additions & 1 deletion server/services/core.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,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
Empty file modified server/services/index.js
100644 → 100755
Empty file.
Empty file modified server/services/lifecycle.js
100644 → 100755
Empty file.
Empty file modified server/services/pattern.js
100644 → 100755
Empty file.
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
Empty file modified server/services/settings.js
100644 → 100755
Empty file.
26 changes: 26 additions & 0 deletions server/utils/copyPublicFolder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { promises: fs } = require("fs");

Check warning on line 1 in server/utils/copyPublicFolder.js

View workflow job for this annotation

GitHub Actions / lint (18)

Strings must use singlequote

Check warning on line 1 in server/utils/copyPublicFolder.js

View workflow job for this annotation

GitHub Actions / lint (20)

Strings must use singlequote
const path = require("path");

Check warning on line 2 in server/utils/copyPublicFolder.js

View workflow job for this annotation

GitHub Actions / lint (18)

Strings must use singlequote

Check warning on line 2 in server/utils/copyPublicFolder.js

View workflow job for this annotation

GitHub Actions / lint (20)

Strings must use singlequote

const copyPublicFolder = async (src, dest) => {
await fs.mkdir(dest, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });

entries.map(async (entry) => {
const srcPath = path.join(src, entry.name);

let destPath = '';
if (entry.name === '.gitignore.example') {
destPath = path.join(dest, '.gitignore');
} else {
destPath = path.join(dest, entry.name);
}

if (entry.isDirectory()) {
await copyPublicFolder(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
}
});
};

module.exports = copyPublicFolder;
Empty file modified server/utils/index.js
100644 → 100755
Empty file.
Empty file modified strapi-admin.js
100644 → 100755
Empty file.
Empty file modified strapi-server.js
100644 → 100755
Empty file.
Empty file modified xsl/sitemap.xsl
100644 → 100755
Empty file.
Empty file modified xsl/sitemap.xsl.css
100644 → 100755
Empty file.
Empty file modified xsl/sitemap.xsl.js
100644 → 100755
Empty file.
Empty file modified xsl/sortable.min.js
100644 → 100755
Empty file.
Loading
Loading