Skip to content

Commit

Permalink
feat: add dynamic filters (#494)
Browse files Browse the repository at this point in the history
* feat: add dynamic filters

* remove console

* fix e2e
  • Loading branch information
foyarash authored Dec 13, 2024
1 parent b762132 commit 07d8996
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-ads-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@premieroctet/next-admin": minor
---

feat: add dynamic filters (#491)
8 changes: 8 additions & 0 deletions apps/docs/pages/docs/api/model-configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions apps/example/options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
6 changes: 5 additions & 1 deletion packages/next-admin/src/components/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import clsx from "clsx";
import { useState } from "react";
import { useEffect, useState } from "react";
import Checkbox from "./radix/Checkbox";

type BadgeProps = {
Expand All @@ -15,6 +15,10 @@ const Badge = ({ isActive, onClick, ...props }: BadgeProps) => {
onClick?.(e);
};

useEffect(() => {
setActive(isActive);
}, [isActive]);

return (
<div
{...props}
Expand Down
37 changes: 28 additions & 9 deletions packages/next-admin/src/components/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,39 @@ const Filters = <T extends ModelName>({
}, [query?.filters]);

Check warning on line 38 in packages/next-admin/src/components/Filters.tsx

View workflow job for this annotation

GitHub Actions / start

React Hook useEffect has a missing dependency: 'fetchUrlFilter'. Either include it or remove the dependency array

Check warning on line 38 in packages/next-admin/src/components/Filters.tsx

View workflow job for this annotation

GitHub Actions / Release

React Hook useEffect has a missing dependency: 'fetchUrlFilter'. Either include it or remove the dependency array

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),
},
});
};
Expand Down
10 changes: 6 additions & 4 deletions packages/next-admin/src/components/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useDeleteAction } from "../hooks/useDeleteAction";
import { useRouterInternal } from "../hooks/useRouterInternal";
import {
AdminComponentProps,
FilterWrapper,
ListData,
ListDataItem,
ModelIcon,
Expand Down Expand Up @@ -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<ModelName>[];
if (
!(modelOptions?.list?.search && modelOptions?.list?.search?.length === 0)
) {
Expand Down Expand Up @@ -271,9 +273,9 @@ function List({
}}
>
<SelectTrigger className="bg-nextadmin-background-default dark:bg-dark-nextadmin-background-subtle max-h-[36px] max-w-[100px]">
<span className="text-nextadmin-content-inverted dark:text-dark-nextadmin-content-inverted pointer-events-none">
{pageSize}
</span>
<span className="text-nextadmin-content-inverted dark:text-dark-nextadmin-content-inverted pointer-events-none">
{pageSize}
</span>
</SelectTrigger>
<SelectContent>
<SelectItem value={"10"}>10</SelectItem>
Expand Down
6 changes: 5 additions & 1 deletion packages/next-admin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ export type FilterWrapper<T extends ModelName> = {
* @link https://www.prisma.io/docs/orm/reference/prisma-client-reference#filter-conditions-and-operators
*/
value: Filter<T>;
/**
* An id that will be used to give filters with the same group name a radio like behavior
*/
group?: string;
};

type RelationshipSearch<T> = {
Expand Down Expand Up @@ -428,7 +432,7 @@ export type ListOptions<T extends ModelName> = {
/**
* define a set of Prisma filters that user can choose in list
*/
filters?: FilterWrapper<T>[];
filters?: Array<FilterWrapper<T> | (() => Promise<FilterWrapper<T>[]>)>;
/**
* define a set of Prisma filters that are always active in list
*/
Expand Down
41 changes: 33 additions & 8 deletions packages/next-admin/src/utils/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Enumeration,
Field,
Filter,
FilterWrapper,
ListOptions,
Model,
ModelName,
Expand Down Expand Up @@ -251,12 +252,34 @@ const getWherePredicateFromQueryParams = (query: string) => {
return validateQuery(query);
};

const preparePrismaListRequest = <M extends ModelName>(
export const mapModelFilters = async (
filters: ListOptions<ModelName>["filters"]
): Promise<FilterWrapper<ModelName>[]> => {
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 <M extends ModelName>(
resource: M,
searchParams: any,
options?: NextAdminOptions,
skipFilters: boolean = false
): PrismaListRequest<M> => {
): Promise<PrismaListRequest<M>> => {
const model = globalSchema.definitions[
resource
] as SchemaDefinitions[ModelName];
Expand All @@ -275,7 +298,11 @@ const preparePrismaListRequest = <M extends ModelName>(

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);
Expand Down Expand Up @@ -331,10 +358,7 @@ const preparePrismaListRequest = <M extends ModelName>(
resource,
options,
search,
otherFilters: [
...fieldFilters ?? [],
...list?.where ?? []
],
otherFilters: [...(fieldFilters ?? []), ...(list?.where ?? [])],
advancedSearch,
});

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -674,6 +698,7 @@ export const getDataItem = async <M extends ModelName>({
select,
where: { [idProperty]: resourceId },
});

Object.entries(data).forEach(([key, value]) => {
if (Array.isArray(value)) {
const fieldType =
Expand Down
9 changes: 8 additions & 1 deletion packages/next-admin/src/utils/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
);
Expand Down

0 comments on commit 07d8996

Please sign in to comment.