diff --git a/.changeset/sweet-ads-drum.md b/.changeset/sweet-ads-drum.md
new file mode 100644
index 00000000..1489107b
--- /dev/null
+++ b/.changeset/sweet-ads-drum.md
@@ -0,0 +1,5 @@
+---
+"@premieroctet/next-admin": minor
+---
+
+feat: add dynamic filters (#491)
diff --git a/apps/docs/pages/docs/api/model-configuration.mdx b/apps/docs/pages/docs/api/model-configuration.mdx
index 3aac70b3..25a62741 100644
--- a/apps/docs/pages/docs/api/model-configuration.mdx
+++ b/apps/docs/pages/docs/api/model-configuration.mdx
@@ -277,9 +277,17 @@ The `filters` property allow you to define a set of Prisma filters that user can
>
),
},
+ {
+ name: "group",
+ type: "String",
+ description:
+ "an id that will be used to give filters with the same group name a radio like behavior",
+ },
]}
/>
+It can also be an async function that returns the above type so that you can have a dynamic list of filters.
+
#### `list.exports` property
The `exports` property is available in the `list` property. It's an object or an array of objects that can have the following properties:
diff --git a/apps/example/options.tsx b/apps/example/options.tsx
index c00cbf70..70dd332a 100644
--- a/apps/example/options.tsx
+++ b/apps/example/options.tsx
@@ -252,6 +252,19 @@ export const options: NextAdminOptions = {
published: false,
},
},
+ async function byCategoryFilters() {
+ const categories = await prisma.category.findMany({
+ select: { id: true, name: true },
+ take: 5,
+ });
+
+ return categories.map((category) => ({
+ name: category.name,
+ value: { categories: { some: { id: category.id } } },
+ active: false,
+ group: "by_category_id",
+ }));
+ },
],
search: ["title", "content", "tags", "author.name"],
fields: {
diff --git a/packages/next-admin/src/components/Badge.tsx b/packages/next-admin/src/components/Badge.tsx
index 2bce54fb..5ed06364 100644
--- a/packages/next-admin/src/components/Badge.tsx
+++ b/packages/next-admin/src/components/Badge.tsx
@@ -1,5 +1,5 @@
import clsx from "clsx";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import Checkbox from "./radix/Checkbox";
type BadgeProps = {
@@ -15,6 +15,10 @@ const Badge = ({ isActive, onClick, ...props }: BadgeProps) => {
onClick?.(e);
};
+ useEffect(() => {
+ setActive(isActive);
+ }, [isActive]);
+
return (
({
}, [query?.filters]);
const toggleFilter = (name: string) => {
+ const toggledFilter = filters?.find((filter) => filter.name === name);
+
+ if (!toggledFilter) return;
+
+ const newFiltersNames = currentFilters
+ ?.map((filter) => {
+ let isActive = filter.active;
+
+ if (filter.name === name) {
+ isActive = !filter.active;
+ }
+
+ if (
+ filter.name !== toggledFilter.name &&
+ filter.group === toggledFilter.group
+ ) {
+ isActive = false;
+ }
+
+ return {
+ ...filter,
+ active: isActive,
+ };
+ })
+ .filter((filter) => filter.active)
+ .map((filter) => filter.name);
+
router?.push({
pathname: location.pathname,
query: {
...query,
page: 1,
- filters: JSON.stringify(
- currentFilters
- ?.map((filter) => ({
- ...filter,
- active: filter.name === name ? !filter.active : filter.active,
- }))
- .filter((filter) => filter.active)
- .map((filter) => filter.name)
- ),
+ filters: JSON.stringify(newFiltersNames),
},
});
};
diff --git a/packages/next-admin/src/components/List.tsx b/packages/next-admin/src/components/List.tsx
index c4f3cc58..87e24738 100644
--- a/packages/next-admin/src/components/List.tsx
+++ b/packages/next-admin/src/components/List.tsx
@@ -14,6 +14,7 @@ import { useDeleteAction } from "../hooks/useDeleteAction";
import { useRouterInternal } from "../hooks/useRouterInternal";
import {
AdminComponentProps,
+ FilterWrapper,
ListData,
ListDataItem,
ModelIcon,
@@ -91,7 +92,8 @@ function List({
const hasDeletePermission =
!modelOptions?.permissions || modelOptions?.permissions?.includes("delete");
- const filterOptions = modelOptions?.list?.filters;
+ const filterOptions = modelOptions?.list
+ ?.filters as FilterWrapper
[];
if (
!(modelOptions?.list?.search && modelOptions?.list?.search?.length === 0)
) {
@@ -271,9 +273,9 @@ function List({
}}
>
-
- {pageSize}
-
+
+ {pageSize}
+
10
diff --git a/packages/next-admin/src/types.ts b/packages/next-admin/src/types.ts
index d0c6f3c4..1daa3e18 100644
--- a/packages/next-admin/src/types.ts
+++ b/packages/next-admin/src/types.ts
@@ -205,6 +205,10 @@ export type FilterWrapper = {
* @link https://www.prisma.io/docs/orm/reference/prisma-client-reference#filter-conditions-and-operators
*/
value: Filter;
+ /**
+ * An id that will be used to give filters with the same group name a radio like behavior
+ */
+ group?: string;
};
type RelationshipSearch = {
@@ -428,7 +432,7 @@ export type ListOptions = {
/**
* define a set of Prisma filters that user can choose in list
*/
- filters?: FilterWrapper[];
+ filters?: Array | (() => Promise[]>)>;
/**
* define a set of Prisma filters that are always active in list
*/
diff --git a/packages/next-admin/src/utils/prisma.ts b/packages/next-admin/src/utils/prisma.ts
index 47ec1a15..6f04f6d3 100644
--- a/packages/next-admin/src/utils/prisma.ts
+++ b/packages/next-admin/src/utils/prisma.ts
@@ -7,6 +7,7 @@ import {
Enumeration,
Field,
Filter,
+ FilterWrapper,
ListOptions,
Model,
ModelName,
@@ -251,12 +252,34 @@ const getWherePredicateFromQueryParams = (query: string) => {
return validateQuery(query);
};
-const preparePrismaListRequest = (
+export const mapModelFilters = async (
+ filters: ListOptions["filters"]
+): Promise[]> => {
+ if (!filters) {
+ return [];
+ }
+
+ const newFilters = await Promise.all(
+ filters.map(async (filter) => {
+ if (typeof filter === "function") {
+ const asyncFilters = await filter();
+
+ return asyncFilters;
+ }
+
+ return filter;
+ })
+ );
+
+ return newFilters.flat().filter(Boolean);
+};
+
+const preparePrismaListRequest = async (
resource: M,
searchParams: any,
options?: NextAdminOptions,
skipFilters: boolean = false
-): PrismaListRequest => {
+): Promise> => {
const model = globalSchema.definitions[
resource
] as SchemaDefinitions[ModelName];
@@ -275,7 +298,11 @@ const preparePrismaListRequest = (
const fieldSort = options?.model?.[resource]?.list?.defaultSort;
- const fieldFilters = options?.model?.[resource]?.list?.filters
+ const filters = await mapModelFilters(
+ options?.model?.[resource]?.list?.filters
+ );
+
+ const fieldFilters = filters
?.filter((filter) => {
if (Array.isArray(filtersParams)) {
return filtersParams.includes(filter.name);
@@ -331,10 +358,7 @@ const preparePrismaListRequest = (
resource,
options,
search,
- otherFilters: [
- ...fieldFilters ?? [],
- ...list?.where ?? []
- ],
+ otherFilters: [...(fieldFilters ?? []), ...(list?.where ?? [])],
advancedSearch,
});
@@ -443,7 +467,7 @@ const fetchDataList = async (
{ prisma, resource, options, searchParams }: FetchDataListParams,
skipFilters: boolean = false
) => {
- const prismaListRequest = preparePrismaListRequest(
+ const prismaListRequest = await preparePrismaListRequest(
resource,
searchParams,
options,
@@ -674,6 +698,7 @@ export const getDataItem = async ({
select,
where: { [idProperty]: resourceId },
});
+
Object.entries(data).forEach(([key, value]) => {
if (Array.isArray(value)) {
const fieldType =
diff --git a/packages/next-admin/src/utils/props.ts b/packages/next-admin/src/utils/props.ts
index 02d29f48..fd8cd69d 100644
--- a/packages/next-admin/src/utils/props.ts
+++ b/packages/next-admin/src/utils/props.ts
@@ -12,7 +12,7 @@ import {
NextAdminOptions,
} from "../types";
import { getCustomInputs } from "./options";
-import { getDataItem, getMappedDataList } from "./prisma";
+import { getDataItem, getMappedDataList, mapModelFilters } from "./prisma";
import {
applyVisiblePropertiesInSchema,
getEnableToExecuteActions,
@@ -130,6 +130,13 @@ export async function getPropsFromParams({
appDir: isAppDir,
});
+ if (options?.model?.[resource]?.list?.filters) {
+ // @ts-expect-error
+ clientOptions.model[resource].list.filters = await mapModelFilters(
+ options.model![resource]!.list!.filters
+ );
+ }
+
const dataIds = data.map(
(item) => item[getModelIdProperty(resource)].value
);