npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@cslegany/multi-select-filter-strapi5

v0.5.10

Published

A customizable Multi-Select Filter to replace the default one.

Downloads

749

Readme

strapi5-multi-select-filter

This package adds a customizable Multi-Select Filter to replace the default one.

Installation

NPM:

npm install @cslegany/multi-select-filter-strapi5

Yarn:

yarn add @cslegany/multi-select-filter-strapi5

Usage

  • This plugin was created becase we wanted to display a list of articles on a Single Type Page to get something like a list of selected articles for Featured News.
  • Default Settings of this plugin are accessible via Settings / Multi Select Filter / Configuration. You can set a default Entity Uid (i.e. api::article.article), a custom api endpoint, the option to show only published items and a query limit. The plugin can update a potentially hidden field on the Single Type Page form to ensure that Save and Publish buttons of the form work properly.
  • You have to register Multi-Select Filter to the Single Type as a custom field. Set a unique name for each instance of your custom field. You can override default settings here.
  • Thereafter you can use the plugin on the Single Type Page. It supports infinite loading and query limit is the maximum amount of items loaded in one batch.
  • It is highly recommended to show only published items since you don't want to display drafts in a Featured News section.
  • If you don't provide an Api Endpoint, a basic query will be used by the plugin to query all items specified by the Entity Uid and sort them by the entity's mainField property.

Custom Api Endpoint

You can write your custom api endpoint for your main Strapi project to suit your business needs. The following is an example that supports a scheduled_at field for articles. Logic is that you need only those articles which were scheduled before the current date and we suppose that scheduled_at always has a value. Meta pagination info is needed for infinite scrolling. Return value of the service is fixed, it has to contain an object

{
  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 <FeaturedNewsResult>{
      left_articles: leftArticles.map(x => <DocumentResponse>{ id: x.id, documentId: x.documentId }),
      main_articles: mainArticles.map(x => <DocumentResponse>{ id: x.id, documentId: x.documentId }),
      right_articles: rightArticles.map(x => <DocumentResponse>{ 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<string, Settings> = 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<T> = Event & {
	result?: T; // since result is only available in `afterXXX` events
}
type MyEvent = LifecycleEvent<Attribute.GetValues<'api::highlight-setting.highlight-setting'>>;

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 ?? [];