🌐 AI搜索 & 代理 主页
Skip to content

DomZem/nextjs-panel-admin

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Installation

Clone project

git clone https://github.com/DomZem/nextjs-panel-admin

Go to the project directory

cd nextjs-panel-admin

Install dependencies

npm install

Create dev db

docker compose up -d

Seed dev db

npm run seed:dev

Run dev app

npm run dev

Todo List

  • Feat: add saving selected columns to display in local storage
  • Feat: add scroll to sheet
  • Feat: add option to add default value for field through prop in auto form
  • Feat: add combobox to choose relation data. Ex. adding user to order, or adding product to order_item model
  • Feat: add another variant of auto table without required onDetails and renderDetails
  • Feat: add wysiwg editor as input to auto form
  • Feat: add component for date input
  • Fix: value prop on input should not be null. Consider using an empty string to clear the component or undefined for uncontrolled components It appears on update product
  • Feat: add to each input in auto form which can be nullable button that will set the field to null
  • Feat: add filters between header and table
  • Fix: after refresh selected row disappear
  • Feat: added columns map to auto table. By adding that i will be able to modify the end result of column. Like for example rendering user avatar
  • Feat: add saving columns order in local storage for dnd table
  • Fix: add clear option to filters
  • Fix: fix issue with clearing values in update form for nullable inputs
  • Fix: after add some fields to existing models, dragging columns is not possible
  • Test: add basic tests for AutoForm in jest
  • Fix: on mobile view filters takes a lot of space. We should hide them some where
  • Feat: replace date-fns by dayjs in date time picker
  • Test: add some tests for auto table
  • Fix: fix issue with no handle discriminated unions in auto form
  • Fix: clear value in auto form for textarea not working
  • Feat: add lazy and eager loading for renderDetails
  • Feat: add mapping labels for discriminator and description
  • Feat: add clearing field for combobox
  • Feat: add multiple delete
  • Feat: add default selected columns

Recommendation

🗂 Folder Structure

src/
├── common/
│   └── validations/
│       └── [module]/
│           └── model-name.ts                          # Zod schemas shared between tRPC procedures and components
│
├── components/
│   └── features/
│       └── [module]/
│           ├── model-name-[table | filters].tsx       # Feature components like CRUD tables, filters, etc.
│           ├── users-table.tsx                        # (example) CRUD table for users
│           └── user-filters.tsx                       # (example) Filter UI for user module
│
├── server/
│   └── api/
│       └── routers/
│           └── [module]/
│               └── model-name.ts                      # tRPC router definition for the module

Common flow with independent model

Create validation

Create model-name.ts file inside validation folder.
For example let's create region.ts validation file for region model.

import { RegionScalarSchema } from "~/zod-schemas/models";

export const regionSchema = RegionScalarSchema;

export const regionFormSchema = regionSchema
  .omit({
    created_at: true,
    updated_at: true,
  })
  .partial({
    id: true,
  });
💡 Tip: Whenever you are creating validation for independent model use ScalarSchema

Create router

Create model-name.ts file inside server/api/routers folder.
For example let's create region.ts router file for region model.

import { regionFormSchema } from "~/common/validations/region/region";
import { adminProcedure, createTRPCRouter } from "../../trpc";
import { RegionScalarSchema } from "~/zod-schemas/models";

export const regionRouter = createTRPCRouter({
  getAll: adminProcedure.query(async ({ ctx }) => {
    const regions = await ctx.db.region.findMany({
      orderBy: {
        id: "asc",
      },
    });

    return regions;
  }),
  getOne: adminProcedure
    .input(RegionScalarSchema.pick({ id: true }))
    .mutation(async ({ ctx, input }) => {
      const result = await ctx.db.region.findFirstOrThrow({
        where: {
          id: input.id,
        },
      });

      return result;
    }),
  createOne: adminProcedure
    .input(regionFormSchema.omit({ id: true }))
    .mutation(async ({ ctx, input }) => {
      const result = await ctx.db.region.create({
        data: input,
      });

      return result;
    }),
  updateOne: adminProcedure
    .input(regionFormSchema.required({ id: true }))
    .mutation(async ({ ctx, input }) => {
      const result = await ctx.db.region.update({
        where: {
          id: input.id,
        },
        data: input,
      });

      return result;
    }),
  deleteOne: adminProcedure
    .input(RegionScalarSchema.pick({ id: true }))
    .mutation(async ({ ctx, input }) => {
      const result = await ctx.db.region.delete({
        where: {
          id: input.id,
        },
      });

      return result;
    }),
});

Create table

Create model-name-table.tsx component file inside /components/features/[module-name] folder.
For example let's create regions-table.tsx component file inside /components/features/region.

export const RegionsTable = () => {
  const getAllRegions = api.region.getAll.useQuery();
  const deleteRegion = api.region.deleteOne.useMutation();
  const createRegion = api.region.createOne.useMutation();
  const updateRegion = api.region.updateOne.useMutation();
  const getRegionDetails = api.region.getOne.useMutation();

  return (
    <AutoTableContainer>
      <AutoTableFullActions
        technicalTableName="regions"
        schema={regionSchema}
        rowIdentifierKey="id"
        data={getAllRegions.data ?? []}
        onRefetchData={getAllRegions.refetch}
        onDetails={async (row) =>
          await getRegionDetails.mutateAsync({
            id: row.id,
          })
        }
        onDelete={async (row) => await deleteRegion.mutateAsync({ id: row.id })}
        renderDetails={(region) => {
          return (
            <div className="space-y-4">
              <RegionCountriesTable regionId={region.id} />
            </div>
          );
        }}
        autoForm={{
          formSchema: regionFormSchema,
          fieldsConfig: {
            id: {
              hidden: true,
            },
          },
          create: {
            onCreate: createRegion.mutateAsync,
            isSubmitting: createRegion.isPending,
          },
          update: {
            onUpdate: updateRegion.mutateAsync,
            isSubmitting: updateRegion.isPending,
          },
        }}
      >
        <AutoTableToolbarHeader title="Regions" />

        <AutoTableDndTable
          extraRow={(row) => <AutoTableDetailsRow rowId={row.id} />}
        />
      </AutoTableFullActions>
    </AutoTableContainer>
  );
};

FAQ

How to use combobox as custom input?

First create model-name-combobox.tsx component file inside /components/features/[model-name].
Example:

import { FormItem, FormLabel } from "~/components/ui/form";
import { Combobox } from "~/components/ui/combobox";
import { api } from "~/trpc/react";
import { useState } from "react";

export const UserCombobox = ({
  selectedValue,
  onSelect,
}: {
  selectedValue?: string;
  onSelect: (value: string) => void;
}) => {
  const [searchValue, setSearchValue] = useState("");

  const getSearchUsers = api.user.getSearchUsers.useQuery({
    name: searchValue,
  });

  const userOptions =
    getSearchUsers.data?.map((user) => ({
      value: user.id,
      label: user.name ?? "",
    })) ?? [];

  return (
    <FormItem>
      <FormLabel>user</FormLabel>
      <Combobox
        options={userOptions}
        onInputChange={setSearchValue}
        onSelect={onSelect}
        selectedValue={selectedValue}
        emptyPlaceholder="No user found."
        searchPlaceholder="Search user..."
        selectPlaceholder="Select user..."
      />
    </FormItem>
  );
};
getSearchUsers: adminProcedure
    .input(
      z.object({
        name: z.string().optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      if (!input.name) {
        const users = await ctx.db.user.findMany({
          take: 5,
          orderBy: {
            id: "asc",
          },
          select: {
            id: true,
            name: true,
          },
        });

        return users;
      }

      const filteredUsers = await ctx.db.user.findMany({
        where: {
          name: {
            contains: input.name,
          },
        },
        orderBy: {
          id: "asc",
        },
        select: {
          id: true,
          name: true,
        },
      });

      return filteredUsers;
    }),

Then use that component as custom field type in AutoForm

user_id: {
  type: "custom",
  render: ({ field }) => {
    return (
      <UserCombobox
        selectedValue={field.value}
        onSelect={field.onChange}
      />
    );
  },
},

How to display value from nested object?

In order to display value from nested object you have to merge field to schema and then map getAll method in router.
For example, let's say for order model you want to display which user created order.

const orderSchema = orderRawSchema.merge(
  z.object({
    username: z.string(),
  }),
);
const orders = await ctx.db.order.findMany({
  where,
  skip: (input.page - 1) * input.pageSize,
  take: input.pageSize,
  orderBy: {
    id: "asc",
  },
  include: {
    user: {
      select: {
        name: true,
      },
    },
  },
});
const mappedOrders = orders.map(({ user, ...rest }) => ({
  ...rest,
  username: user.name!,
}));

How to add custom validation for input field?

In order to add custom validation for input field you have to omit that field and then merge with validation.

export const regionFormSchema = regionSchema
  .omit({
    created_at: true,
    updated_at: true,
  })
  .partial({
    id: true,
  })
  .omit({
    name: true,
  })
  .merge(
    z.object({
      name: z.string().min(3).max(255),
    }),
  );

What to do if field type for combobox is not string?

When the field type for comobobox is not a string, but for example the number. All you have to do is:

  1. Allow selectedValue prop inside model-name-combobox.tsx file to be that type.
    export const RegionCountryCombobox = ({
      selectedValue,
      onSelect,
    }: {
      selectedValue?: string | number;
      onSelect: (value: string) => void;
    }) => {};
  2. Add coerce to field that is using combobox as component.
    export const userAddressFormSchema = userAddressSchema
      .omit({
        region_country_id: true,
        created_at: true,
        updated_at: true,
      })
      .merge(
        z.object({
          region_country_id: z.coerce.number(),
        }),
      )
      .partial({
        id: true,
      });

Releases

No releases published

Packages

No packages published

Languages