A customizable Multi-Select Filter to replace the default one.
npm install @cslegany/multi-select-filter-strapi5npm install @cslegany/multi-select-filter-strapi5
yarn add @cslegany/multi-select-filter-strapi5
{
result: {"id", "documentId" }[],
mainField: string,
meta: {
total: number;
pageSize: number;
pageCount: number;
currentPage: number;
} | undefined,
}
`
The api endpoint POST request also has a fixed format and have to supply the following values:
filter: string, publishedOnly?: boolean, queryStart?: number, queryLimit?: number
articlefilter.ts controller is as follows
`
import type * as strapi from '@strapi/strapi';
export default ({ strapi }: { strapi: strapi.Core.Strapi }) => ({
async getFeaturedNews(ctx) {
return await strapi.service("api::article-filter.articlefilter").getFeaturedNews();
},
async getFilteredArticles(ctx) {
const body = ctx.request.body;
return await strapi.service('api::article-filter.articlefilter').getFilteredArticles(
body.filter, body.publishedOnly, body.queryStart, body.queryLimit);
},
})
`
articlefilter.ts route is as follows
`
export default {
routes: [
{
method: 'POST',
path: '/article-filter',
handler: 'articlefilter.getFilteredArticles',
config: {
auth: false,
policies: [],
middlewares: [],
},
},
{
method: 'GET',
path: '/featured-news',
handler: 'articlefilter.getFeaturedNews',
config: {
auth: false,
policies: [],
middlewares: [],
},
}
]
};
`
Now you can develop your own api endpoint to get results from the plugin.
Here we suppose that the Single Type page contains three instances of the custom field named as left_articles, main_articles and right_articles.
articlefilter.ts service is as follows
`
import type * as strapi from '@strapi/strapi';
type Settings = {
mainField: string;
defaultSortBy: string;
defaultSortOrder: string;
};
interface DocumentResponse {
id: number;
documentId: string;
}
interface OrderedDocumentResponse extends DocumentResponse {
order: number;
tag: string;
}
interface GetDocumentsByTagResult {
result: {
uid: string;
items: OrderedDocumentResponse[];
}[];
errorMessage: string;
}
interface FeaturedNewsResult {
left_articles: DocumentResponse[];
main_articles: DocumentResponse[];
right_articles: DocumentResponse[];
}
export default ({ strapi }: { strapi: strapi.Core.Strapi }) => ({
async getFeaturedNews() {
const multiSelectFilter = strapi.plugin("multi-select-filter").service("multiSelectFilter");
const documentResult: GetDocumentsByTagResult = await multiSelectFilter.getDocumentsGroupedByTag();
if (documentResult.errorMessage)
return;
const articleResult = documentResult.result.find(x => x.uid === "api::article.article");
if (!articleResult)
return;
let leftArticles = articleResult.items.filter(x => x.tag === "left_side_multi_select_filter");
leftArticles = leftArticles.sort(x => x.order);
let mainArticles = articleResult.items.filter(x => x.tag === "main_multi_select_filter");
mainArticles = mainArticles.sort(x => x.order);
let rightArticles = articleResult.items.filter(x => x.tag === "right_side_multi_select_filter");
rightArticles = rightArticles.sort(x => x.order);
return {
left_articles: leftArticles.map(x => { id: x.id, documentId: x.documentId }),
main_articles: mainArticles.map(x => { id: x.id, documentId: x.documentId }),
right_articles: rightArticles.map(x => { id: x.id, documentId: x.documentId }),
}
},
async getFilteredArticles(filter: string, publishedOnly?: boolean, queryStart?: number, queryLimit?: number) {
let res = {
result: [],
errorMessage: "",
mainField: "",
};
try {
const { findConfiguration } = strapi.plugin('content-manager').service('content-types');
const { settings }: Record = await findConfiguration(strapi.contentType("api::article.article"));
const { mainField, defaultSortBy, defaultSortOrder } = settings; //defaultSortBy is 'title' in case of an article
const scheduledAtFilter = {
scheduled_at: { $lte: (new Date).toISOString() }
};
const mainFieldFilter = {
[mainField]: {
$contains: filter
}
};
const publishedFilter = {
$and: [
mainFieldFilter,
{ publishedAt: { $notNull: true } },
scheduledAtFilter,
],
};
const notPublishedFilter = {
$and: [
mainFieldFilter,
scheduledAtFilter,
],
};
let filters = (!filter)
? scheduledAtFilter
: publishedOnly
? publishedFilter
: notPublishedFilter;
const start = queryStart ?? 0;
const limit = queryLimit ?? undefined;
const sort = publishedOnly ? scheduled_at:desc : ${defaultSortBy}:${defaultSortOrder};
const total = await strapi.documents("api::article.article").count({
filters,
status: publishedOnly ? 'published' : undefined,
});
const documents = await strapi.documents("api::article.article").findMany({
fields: ["id", "publishedAt", "scheduled_at", mainField] as any,
filters,
status: publishedOnly ? 'published' : undefined,
start,
limit,
sort: sort as any,
});
const hasMeta = limit !== undefined;
const meta = !hasMeta ? undefined : {
total, // gets the total number of records
pageSize: limit, // gets the limit we set earlier
pageCount: Math.ceil(total / limit), // gives us the number of total pages
currentPage: start / limit + 1, // returns the current page
};
return {
result: documents,
mainField,
meta,
}
}
catch (error) {
console.error(error);
res.result = [];
res.errorMessage = error;
}
return res;
}
})
`
Time to develop your Single Type page which will be called Highlight Settings. Generate a Single Type in src/api/highlight-setting.
Adjust schema.json in content-types as follows:
`
{
"kind": "singleType",
"collectionName": "highlight_settings",
"info": {
"singularName": "highlight-setting",
"pluralName": "highlight-settings",
"displayName": "Highlight Settings",
"description": ""
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string"
},
"featured_news_info": {
"type": "string"
},
"featured_news": {
"type": "component",
"repeatable": false,
"component": "highlighted.highlighted-articles"
}
}
}
`
Make a lifecycles.ts file here and add the following code to ensure that multiselect items will get published when you publish your Single Type.
`
import * as Attribute from '@strapi/types/dist/modules/documents/params/attributes';
import { Event } from '@strapi/database/dist/lifecycles';
type LifecycleEvent = Event & {
result?: T; // since result is only available in afterXXX events
}
type MyEvent = LifecycleEvent>;
interface MultiSelectItemId {
id: number;
documentId: string;
}
const publishByTag = async (tag: string) => {
const pluginId = "plugin::multi-select-filter.multiselect";
//delete existing published items
const multiSelectsToDelete = (await strapi.documents(pluginId).findMany({
fields: ['id', 'documentId'] as any,
filters: {
tag: { $eq: tag },
},
status: 'published',
})) as MultiSelectItemId[];
await Promise.all(multiSelectsToDelete.map(async (x) => {
await strapi.documents(pluginId).delete({
documentId: x.documentId,
filters: {
publishedAt: { $notNull: true },
}
})
}))
//publish not published items
const multiSelectsToPublish = (await strapi.documents(pluginId).findMany({
fields: ['id', 'documentId'] as any,
filters: {
$and: [
{ tag: { $eq: tag } },
{ publishedAt: { $null: true } },
]
},
})) as MultiSelectItemId[];
await Promise.all(multiSelectsToPublish.map(async (x) => {
await strapi.documents(pluginId).publish({
documentId: x.documentId,
})
}))
}
export default {
async beforeDelete(event: MyEvent) {
const where = event.params.where;
//we receive the unpublished item's id in where?.id
//get the documentId belonging to that item
const unpublishedItem = await strapi.db.query("api::highlight-setting.highlight-setting").findOne({
where,
select: ['id', 'documentId']
});
//get the id of the published version of this item
const publishedItem = await strapi.documents("api::highlight-setting.highlight-setting").findOne({
documentId: unpublishedItem.documentId,
fields: ['publishedAt'],
status: 'published',
})
//save it to strapi as a temp storage
strapi["highlight-updating-id"] = publishedItem.id;
},
async afterCreate(event: MyEvent) {
// a publish means a delete and create
// if there was a delete beforehand, try to get back our saved id which indicates id of the previous published version of the item
const where = event.params.where;
const item2UpdatingId = strapi["highlight-updating-id"];
const unpublishedItem = await strapi.db.query("api::highlight-setting.highlight-setting").findOne({
where,
select: ['id', 'documentId']
});
const publishedItem = await strapi.documents("api::highlight-setting.highlight-setting").findOne({
documentId: unpublishedItem.documentId,
fields: ['publishedAt'],
populate: ["featured_news"],
status: 'published',
})
//if the current published item isn't the saved one, we published an item
if (publishedItem.id !== item2UpdatingId || item2UpdatingId === undefined) {
const multiFilterTags = Object.keys(publishedItem.featured_news).filter(x => x !== "id");
await Promise.all(multiFilterTags.map(async (tag) => {
await publishByTag(tag)
}))
}
strapi["highlight-updating-id"] = undefined;
}
}
`
Finally add the following code to components / highlighted / highlighted-articles.json to add the component required by the Single Type page.
`
{
"collectionName": "components_highlighted_highlighted_articles",
"info": {
"displayName": "Highlighted Articles",
"icon": "dashboard",
"description": ""
},
"options": {},
"attributes": {
"left_side_multi_select_filter": {
"type": "customField",
"options": {
"publishedOnly": true,
"queryLimit": 20
},
"customField": "plugin::multi-select-filter.multiSelectFilter"
},
"main_multi_select_filter": {
"type": "customField",
"options": {
"publishedOnly": true,
"queryLimit": 20
},
"customField": "plugin::multi-select-filter.multiSelectFilter"
},
"right_side_multi_select_filter": {
"type": "customField",
"options": {
"publishedOnly": true,
"queryLimit": 20
},
"customField": "plugin::multi-select-filter.multiSelectFilter"
}
}
}
`
Here you go, now you can use your api endpoint to get highlighted articles in a Next JS application with the following code:
`
const featuredNews = await fetchAPI(/featured-news, []);
if (!featuredNews)
return null;
const mainArticles = featuredNews.main_articles ?? [];
const leftSideArticles = featuredNews?.left_side_articles ?? [];
const rightSideArticles = featuredNews?.right_side_articles ?? [];
``