SoftwareBrothers / adminjs

AdminJS is an admin panel for apps written in node.js
https://adminjs.co
MIT License
8.06k stars 646 forks source link

Issues while adding custom action components #1677

Closed manohar322028 closed 2 weeks ago

manohar322028 commented 2 weeks ago

Hello, I was adding some custom action components to a record, namely "approve" and "reject" to approve or reject a member.

My issues are: - Issues in the upload components: Error: Component "UploadShowComponent" has not been bundled, ensure it was added to your ComponentLoader instance (the one included in AdminJS options).

(and errors like this in the edit and list as well)

But when I remove the action components, i.e. not add the custom action components, then upload components work fine without any error.

- Issues in the action components: The error says: You have to implement action component for your Action But I have implemented action component exactly like what is shown in the documents.

Given below are my code blocks:

Here's the component loader:

import { ComponentLoader } from "adminjs";

import path from "path";

const componentLoader = new ComponentLoader();

const accept_path = path.resolve("components/ApproveMember.jsx");
const reject_path = path.resolve("components/RejectMember.jsx");

const Components = {
  ApproveMember: componentLoader.add("ApproveMember", accept_path),
  RejectMember: componentLoader.add("RejectMember", reject_path),
};

export { componentLoader, Components };

( I used path.resolve because without using it it was showing 'file doesn't exist' in windows due to extra slashes at the beginning so I had to convert the path to absolute )

No errors were shown during bundling files.

Here's the admin options:

import News from "./resources/news.resource.js";
import Notice from "./resources/notice.resource.js";
import Download from "./resources/download.resource.js";
import About from "./resources/about.resource.js";
import Team from "./resources/team.resource.js";
import Member from "./resources/member.resource.js";
import { componentLoader } from "./component-loader.js";

const adminOptions = {
  resources: [News, Notice, Download, About, Team, Member],
  componentLoader: componentLoader,
  logoutPath: "/admin/logout",
  rootPath: "/admin",
};

export default adminOptions;

Here's the index.js:

import express from "express";
import mongoose from "mongoose";
import dotenv from "dotenv";
import authRoutes from "./routes/auth.route.js";
import newsRoutes from "./routes/news.route.js";
import noticeRoutes from "./routes/notice.route.js";
import downloadRoutes from "./routes/download.route.js";
import aboutRoutes from "./routes/about.route.js";
import teamRoutes from "./routes/team.route.js";
import memberRoutes from "./routes/member.route.js";
import AdminJS from "adminjs";
import AdminJSExpress from "@adminjs/express";
import * as AdminJSMongoose from "@adminjs/mongoose";
import Connect from "connect-mongodb-session";
import session from "express-session";
import User from "./models/user.model.js";
import path from "path";

import bcryptjs from "bcryptjs";

import adminOptions from "./admin.options.js";

import * as url from "url";

const __dirname = url.fileURLToPath(new URL(".", import.meta.url));

dotenv.config();

var database;

mongoose
  .connect(process.env.MONGO_URI)
  .then((x) => {
    database = x;
    console.log("Connected to MongoDB");
  })
  .catch((error) => {
    console.error("Error connecting to MongoDB: ", error);
  });

const ADMIN_USER = await User.findOne({ type: "admin" });

const authenticate = async (email, password) => {
  if (
    email === ADMIN_USER.username &&
    bcryptjs.compareSync(password, ADMIN_USER.password)
  ) {
    return Promise.resolve(ADMIN_USER);
  }
  return null;
};

AdminJS.registerAdapter({
  Resource: AdminJSMongoose.Resource,
  Database: AdminJSMongoose.Database,
});

const start = async () => {
  const app = express();
  app.use(express.json());
  app.use(express.static(path.join(__dirname, "../client/public")));
  app.use(express.static(path.join(__dirname, "../members")));

  const admin = new AdminJS(adminOptions);

  const ConnectSession = Connect(session);
  const sessionStore = new ConnectSession({
    uri: process.env.MONGO_URI,
    collection: "sessions",
  });

  const adminRouter = AdminJSExpress.buildAuthenticatedRouter(
    admin,
    {
      authenticate,
      cookieName: "adminjs",
      cookiePassword: process.env.SESSION_SECRET,
    },
    null,
    {
      store: sessionStore,
      resave: true,
      saveUninitialized: true,
      secret: process.env.SESSION_SECRET,
      cookie: {
        httpOnly: process.env.NODE_ENV === "production",
        secure: process.env.NODE_ENV === "production",
      },
      name: "adminjs",
    }
  );

  app.use(admin.options.rootPath, adminRouter);

  admin.watch();

  app.listen(3000, () => {
    console.log("Server is running on http://localhost:3000");
    console.log(
      `AdminJS started on http://localhost:3000${admin.options.rootPath}`
    );
  });

  app.use("/api/auth", authRoutes);
  app.use("/api/news", newsRoutes);
  app.use("/api/notices", noticeRoutes);
  app.use("/api/downloads", downloadRoutes);
  app.use("/api/abouts", aboutRoutes);
  app.use("/api/teams", teamRoutes);
  app.use("/api/members", memberRoutes);

  app.get("/", (req, res) => {
    res.send("Go to /admin for adminpanel");
  });

  app.use((err, req, res, next) => {
    const statusCode = err.statusCode || 500;
    const message = err.message || "Internal Server Error";
    res.status(statusCode).json({
      success: false,
      statusCode,
      message,
    });
  });
};

start();

below is the code for member.resource where I tried to implement custom actions:

import Member from "../models/member.model.js";
import { componentLoader, Components } from "../component-loader.js";

import { privateLocalProvider } from "../upload-provider.js";
import uploadFeature from "@adminjs/upload";
import path from "path";
import fs from "fs";

export default {
  resource: Member,

  options: {
    actions: {
      approve: {
        icon: "Check",
        label: "Approve",
        actionType: "record",
        component: Components.ApproveMember,
        isVisible: (context) => context.record.get("isApproved") === false,
        handler: async (request, response, context) => {
          request.method = "post";
          return {
            record: context.record.toJSON(context.currentAdmin),
          };
        },
      },
      reject: {
        icon: "Close",
        label: "Reject",
        actionType: "record",
        component: Components.RejectMember,
        isVisible: (context) => context.record.get("isApproved") === false,
        handler: async (request, response, context) => {
          console.log("Reject action");
          return {
            record: context.record.toJSON(context.currentAdmin),
          };
        },
      },
    },

    properties: {
      _id: {
        isVisible: { list: false, show: true, edit: false, filter: false },
      },
      first_name: {
        isVisible: { list: true, show: true, edit: true, filter: true },
        position: 1,
      },
      last_name: {
        isVisible: { list: true, show: true, edit: true, filter: true },
        position: 2,
      },
      isNew: {
        isVisible: { list: false, show: true, edit: true, filter: true },
      },
      isApproved: {
        isVisible: { list: true, show: true, edit: true, filter: true },
      },
      membership_number: {
        isVisible: { list: false, show: true, edit: true, filter: true },
      },
      membership_date: {
        isVisible: { list: false, show: true, edit: true, filter: false },
      },
      membership_certificate: {
        isVisible: { list: false, show: false, edit: false, filter: false },
      },
      membership_certificate_file: {
        isVisible: { list: false, show: true, edit: true, filter: false },
      },
      voucher: {
        isVisible: { list: false, show: false, edit: false, filter: false },
      },
      voucher_file: {
        isVisible: { list: false, show: true, edit: true, filter: false },
      },
      email: {
        isVisible: { list: false, show: true, edit: true, filter: true },
      },
      phone_number: {
        isVisible: { list: false, show: true, edit: true, filter: true },
      },
      district: {
        isVisible: { list: true, show: true, edit: true, filter: true },
      },
      municipality: {
        isVisible: { list: false, show: true, edit: true, filter: true },
      },
      ward: {
        isVisible: { list: false, show: true, edit: true, filter: true },
      },
      school_name: {
        isVisible: { list: true, show: true, edit: true, filter: true },
      },
      school_address: {
        isVisible: { list: true, show: true, edit: true, filter: false },
      },
      school_appointment_date: {
        isVisible: { list: false, show: true, edit: true, filter: false },
      },
      appointment_type: {
        isVisible: { list: false, show: true, edit: true, filter: false },
      },
      createdAt: {
        isVisible: { list: false, show: true, edit: false, filter: false },
      },
      updatedAt: {
        isVisible: { list: false, show: true, edit: false, filter: false },
      },
    },
  },
  features: [
    uploadFeature({
      componentLoader: componentLoader,
      provider: privateLocalProvider,
      properties: {
        key: "voucher",
        bucket: "bucket",
        file: "voucher_file",
        filePath: "filePath",
        filesToDelete: "filesToDelete",
      },
      validation: {
        mimeTypes: ["image/jpeg", "image/png", "image/jpg"],
      },
      uploadPath: (record, filename) => {
        const firstName = record.get("first_name");
        const lastName = record.get("last_name");
        const extension = path.extname(filename);
        let baseFilename = `${firstName}-${lastName}`;
        let index = 0;
        let uniqueFilename = `${baseFilename}${extension}`;
        const folderPath = "./members/uploads/vouchers";

        // Function to check if the file exists synchronously
        const fileExistsSync = (filePath) => {
          try {
            fs.accessSync(filePath, fs.constants.F_OK);
            return true;
          } catch (err) {
            return false;
          }
        };

        // Check for file existence and create a unique filename
        while (fileExistsSync(path.join(folderPath, uniqueFilename))) {
          index += 1;
          uniqueFilename = `${baseFilename}-${index}${extension}`;
        }

        return `vouchers/${uniqueFilename}`;
      },
    }),

    uploadFeature({
      componentLoader: componentLoader,
      provider: privateLocalProvider,
      properties: {
        key: "membership_certificate",
        bucket: "_bucket",
        file: "membership_certificate_file",
        filePath: "_filePath",
        filesToDelete: "_filesToDelete",
      },
      validation: {
        mimeTypes: ["image/jpg", "image/jpeg", "image/png"],
      },
      uploadPath: (record, filename) => {
        const firstName = record.get("first_name");
        const lastName = record.get("last_name");
        const extension = path.extname(filename);
        let baseFilename = `${firstName}-${lastName}`;
        let index = 0;
        let uniqueFilename = `${baseFilename}${extension}`;
        const folderPath = "./members/uploads/certificates";

        // Function to check if the file exists synchronously
        const fileExistsSync = (filePath) => {
          try {
            fs.accessSync(filePath, fs.constants.F_OK);
            return true;
          } catch (err) {
            return false;
          }
        };

        // Check for file existence and create a unique filename
        while (fileExistsSync(path.join(folderPath, uniqueFilename))) {
          index += 1;
          uniqueFilename = `${baseFilename}-${index}${extension}`;
        }

        return `certificates/${uniqueFilename}`;
      },
    }),
  ],
};

below is the code for ApproveMember:

import React from "react";
import { Box, FormGroup, Label, Input, Button } from "@adminjs/design-system";
import Member from "./models/member.model.js";

const ApproveMember = (props) => {
  // const { record } = props;
  // const handleSubmit = async (event) => {
  //   event.preventDefault();
  //   const formData = new FormData(event.target);
  //   const membershipNumber = formData.get("membership_number");
  //   await Member.findOneAndUpdate(
  //     { _id: record.get("_id") },
  //     { membershipNumber }
  //   );
  // };
  console.log("from component : ", props);
  return (
    <Box variant="white" width={1 / 2} p="lg" m="auto" mt="xxl">
      <form onSubmit={handleSubmit}>
        <FormGroup>
          <Label htmlFor="membership_number">Membership Number</Label>
          <Input
            id="membership_number"
            name="membership_number"
            placeholder="Enter membership number"
          />
        </FormGroup>
        <Button variant="primary" mt="md" type="submit">
          Submit
        </Button>
      </form>
    </Box>
  );
};

export default ApproveMember;

and given below is the code for RejectMember:

import React from "react";
import {
  Box,
  FormGroup,
  Label,
  TextArea,
  Button,
} from "@adminjs/design-system";

const RejectMember = () => {
  return (
    <Box variant="white" width={1 / 2} p="lg" m="auto" mt="xxl">
      <form>
        <FormGroup>
          <Label htmlFor="rejection_message">Rejection Message</Label>
          <TextArea
            id="rejection_message"
            name="rejection_message"
            placeholder="Enter rejection message"
          />
        </FormGroup>
        <Button variant="primary" mt="md" type="submit">
          Submit
        </Button>
      </form>
    </Box>
  );
};

export default RejectMember;

Any help would be highly appreciated.

manohar322028 commented 2 weeks ago

I solved it, just by writing the components in tsx instead of jsx. However, the documentation said that it also supports .jsx components but it didn't work out for me.