next.jspayload-cms

Issues integrating S3Storage plugin in PayloadCMS


Been busy trying to setup payload in my application but ran into a roadblock with the S3Storage plugin and I'm at a loss to figure out why this doesn't work with my application but it does work with a fresh payload template build.

I'm trying to use the S3Storage plugin but when I try to access /admin it runs me an error like this: Error: useUploadHandlers must be used within UploadHandlersProvider

Now UploadHandlersProvider isn't something that we handle looking through the code when we have it constructed in layout.tsx its part of the RootLayout

I'm using NextJS 15.2.1 and PayloadCMS 3.26.0

NextJS Error - useUploadHandlers must be used within UploadHandersProvider

/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. *//* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from "@payload-config";
import "@payloadcms/next/css";
import type { ServerFunctionClient } from "payload";
import { handleServerFunctions, RootLayout } from "@payloadcms/next/layouts";
import React from "react";

import { importMap } from "./admin/importMap.js";
import "./custom.scss";

type Args = {
  children: React.ReactNode;
};

const serverFunction: ServerFunctionClient = async function (args) {
  "use server";
  return handleServerFunctions({
    ...args,
    config,
    importMap,
  });
};

const Layout = ({ children }: Args) => (
  <RootLayout
    config={config}
    importMap={importMap}
    serverFunction={serverFunction}
  >
    This is the root layout
    {children}
  </RootLayout>
);

export default Layout;



/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from "@payload-config";
import "@payloadcms/next/css";
import type { ServerFunctionClient } from "payload";
import { handleServerFunctions, RootLayout } from "@payloadcms/next/layouts";
import React from "react";

import { importMap } from "./admin/importMap.js";
import "./custom.scss";

type Args = {
  children: React.ReactNode;
};

const serverFunction: ServerFunctionClient = async function (args) {
  "use server";
  return handleServerFunctions({
    ...args,
    config,
    importMap,
  });
};

const Layout = ({ children }: Args) => (
  <RootLayout
    config={config}
    importMap={importMap}
    serverFunction={serverFunction}
  >
    This is the root layout
    {children}
  </RootLayout>
);

export default Layout;

From doing a comparison to the template, this is almost the same.

For reference, my payload.config.ts looks like

// storage-adapter-import-placeholder// storage-adapter-import-placeholder
import { postgresAdapter } from "@payloadcms/db-postgres";

import sharp from "sharp"; // sharp-import
import path from "path";
import { buildConfig } from "payload";
import { fileURLToPath } from "url";

import { Categories } from "./collections/Categories";
import { Media } from "./collections/Media";
import { Pages } from "./collections/Pages";
import { Posts } from "./collections/Posts";
import { defaultLexical } from "./fields/defaultLexical";
import { getServerSideURL } from "./utils/getURL";

import { Header } from "./Header/config";
import { plugins } from "./plugins";
import { Users } from "./collections/Users";
import CustomFooter from "@tcmarket/collections/Footer";
import { nodemailerAdapter } from "@payloadcms/email-nodemailer";
import nodemailer from "nodemailer";

const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);

export default buildConfig({
  email: nodemailerAdapter({
    defaultFromAddress: process.env.EMAIL_FROM || "",
    defaultFromName: "Mail System",
    transport: await nodemailer.createTransport({
      host: process.env.EMAIL_HOST || "localhost",
      port: parseInt(process.env.EMAIL_PORT || "1025"),
      secure: false, // true for 465, false for other ports
      auth: {
        user: process.env.EMAIL_USERNAME || "1234", // Your email id
        pass: process.env.EMAIL_PASSWORD || "abc", // Your password
      },
    }),
  }),
  graphQL: {
    disable: true,
  },
  telemetry: false,
  admin: {
    components: {
      // The `BeforeLogin` component renders a message that you see while logging into your admin panel.
      // Feel free to delete this at any time. Simply remove the line below and the import `BeforeLogin` statement on line 15.
      beforeLogin: ["@tcmarket/components/BeforeLogin"],
      // The `BeforeDashboard` component renders the 'welcome' block that you see after logging into your admin panel.
      // Feel free to delete this at any time. Simply remove the line below and the import `BeforeDashboard` statement on line 15.
      beforeDashboard: ["@tcmarket/components/BeforeDashboard"],
    },
    importMap: {
      baseDir: path.resolve(dirname),
    },
    user: Users.slug,
    livePreview: {
      breakpoints: [
        {
          label: "Mobile",
          name: "mobile",
          width: 375,
          height: 667,
        },
        {
          label: "Tablet",
          name: "tablet",
          width: 768,
          height: 1024,
        },
        {
          label: "Desktop",
          name: "desktop",
          width: 1440,
          height: 900,
        },
      ],
    },
  },
  // This config helps us configure global or default features that the other editors can inherit
  editor: defaultLexical,
  db: postgresAdapter({
    pool: {
      connectionString: process.env.DATABASE_URI || "",
    },
    push: false,
  }),
  collections: [Pages, Posts, Media, Categories, Users, CustomFooter],
  cors: [getServerSideURL()].filter(Boolean),
  globals: [Header],
  plugins: [
    ...plugins,
    // storage-adapter-placeholder
  ],
  secret: process.env.PAYLOAD_SECRET || "",
  sharp,
  typescript: {
    outputFile: path.resolve(dirname, "./payload-types.ts"),
  },
});

And my plugins:

import { payloadCloudPlugin } from "@payloadcms/payload-cloud";
import { formBuilderPlugin } from "@payloadcms/plugin-form-builder";
import { nestedDocsPlugin } from "@payloadcms/plugin-nested-docs";
import { redirectsPlugin } from "@payloadcms/plugin-redirects";
import { seoPlugin } from "@payloadcms/plugin-seo";
import { searchPlugin } from "@payloadcms/plugin-search";
import { Plugin } from "payload";
import { GenerateTitle, GenerateURL } from "@payloadcms/plugin-seo/types";
import {
  FixedToolbarFeature,
  HeadingFeature,
  lexicalEditor,
} from "@payloadcms/richtext-lexical";
import { s3Storage } from '@payloadcms/storage-s3'
import { revalidateRedirects } from "@tcmarket/hooks/revalidateRedirects";
import { searchFields } from "@tcmarket/search/fieldOverrides";
import { beforeSyncWithSearch } from "@tcmarket/search/beforeSync";
import { Page, Post } from "@tcmarket/payload-types";
import { getServerSideURL } from "@tcmarket/utils/getURL";

const generateTitle: GenerateTitle<Post | Page> = ({ doc }) => {
  return doc?.title
    ? `${doc.title} | Payload Website Template`
    : "Payload Website Template";
};

const generateURL: GenerateURL<Post | Page> = ({ doc }) => {
  const url = getServerSideURL();

  return doc?.slug ? `${url}/${doc.slug}` : url;
};

export const plugins: Plugin[] = [
  redirectsPlugin({
    collections: ["pages", "posts"],
    overrides: {
      fields: ({ defaultFields }) => {
        return defaultFields.map((field) => {
          if ("name" in field && field.name === "from") {
            return {
              ...field,
              admin: {
                components: {},
                description:
                  "You will need to rebuild the website when changing this field.",
              },
            };
          }
          return field;
        });
      },
      hooks: {
        afterChange: [revalidateRedirects],
      },
    },
  }),
  nestedDocsPlugin({
    collections: ["categories"],
  }),
  seoPlugin({
    generateTitle,
    generateURL,
  }),
  formBuilderPlugin({
    fields: {
      payment: false,
    },
    formOverrides: {
      fields: ({ defaultFields }) => {
        return defaultFields.map((field) => {
          if ("name" in field && field.name === "confirmationMessage") {
            return {
              ...field,
              editor: lexicalEditor({
                features: ({ rootFeatures }) => {
                  return [
                    ...rootFeatures,
                    FixedToolbarFeature(),
                    HeadingFeature({
                      enabledHeadingSizes: ["h1", "h2", "h3", "h4"],
                    }),
                  ];
                },
              }),
            };
          }
          return field;
        });
      },
    },
  }),
  searchPlugin({
    collections: ["posts"],
    beforeSync: beforeSyncWithSearch,
    searchOverrides: {
      fields: ({ defaultFields }) => {
        return [...defaultFields, ...searchFields];
      },
    },
  }),
  payloadCloudPlugin(),
  s3Storage({
      collections: {
        media: true,
      },
      bucket: process.env.CLOUDFLARE_R2_BUCKET_NAME || 
      "",
    config: {
        endpoint: process.env.CLOUDFLARE_R2_ENDPOINT || "",
        credentials: {
          accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID || "",
          secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_KEY || "",
        },
        region: "auto",

      },
    }),
];

Solution

  • Looks like updating with the latest change today fixes this! 🕺

    Just entered 3.27.0 and crossed my fingers!