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: add dynamic filters #494

Merged
merged 3 commits into from
Dec 13, 2024
Merged
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
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 @@ -35,23 +35,42 @@

useEffect(() => {
setCurrentFilters(fetchUrlFilter());
}, [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

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
Loading