Compare commits
42 Commits
v3.0.0-alp
...
v3.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca832a01cb | ||
|
|
aaa2b204ba | ||
|
|
78bf9e5993 | ||
|
|
8be0296fc1 | ||
|
|
26cd741c04 | ||
|
|
bf655b3327 | ||
|
|
1793b37adc | ||
|
|
d0ffe85abb | ||
|
|
f9f7dcfc58 | ||
|
|
f06257e7ff | ||
|
|
e490f0bce6 | ||
|
|
770c7173ec | ||
|
|
661ab4867b | ||
|
|
9ee3b5aae6 | ||
|
|
d202256c30 | ||
|
|
b8856d4ef7 | ||
|
|
e1294ac210 | ||
|
|
b0edd2d137 | ||
|
|
0d0e9bc953 | ||
|
|
2576291d9f | ||
|
|
d2aab87faa | ||
|
|
c2509b462c | ||
|
|
0e378be769 | ||
|
|
2785eaab21 | ||
|
|
8a10fd1547 | ||
|
|
8f8ed817fb | ||
|
|
7a150254fe | ||
|
|
9612a4a781 | ||
|
|
249b233dc2 | ||
|
|
c4d4a9b47b | ||
|
|
a55e991bfa | ||
|
|
610276f66b | ||
|
|
de99aabf7f | ||
|
|
40a0a0083f | ||
|
|
56ecd2ac14 | ||
|
|
4617d58b6a | ||
|
|
98aeff2f3e | ||
|
|
f2239decca | ||
|
|
36bd25a9cc | ||
|
|
933ae663f0 | ||
|
|
a08674f708 | ||
|
|
7fe0855932 |
@@ -1,4 +1,4 @@
|
||||
const { withPayload } = require('./packages/next/src/withPayload')
|
||||
const withPayload = require('./packages/next/src/withPayload')
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"private": true,
|
||||
"workspaces:": [
|
||||
"packages/*"
|
||||
@@ -9,7 +9,7 @@
|
||||
"build": "pnpm run build:core",
|
||||
"build:all": "turbo build",
|
||||
"build:core": "turbo build --filter \"!@payloadcms/plugin-*\"",
|
||||
"build:plugins": "turbo build --filter \"@payloadcms/plugin-*\"",
|
||||
"build:plugins": "turbo build --filter \"@payloadcms/plugin-*\" --filter \"!@payloadcms/plugin-search\"",
|
||||
"build:app": "next build",
|
||||
"build:create-payload-app": "turbo build --filter create-payload-app",
|
||||
"build:db-mongodb": "turbo build --filter db-mongodb",
|
||||
@@ -89,6 +89,7 @@
|
||||
"@types/testing-library__jest-dom": "5.14.8",
|
||||
"add-stream": "^1.0.0",
|
||||
"chalk": "^4.1.2",
|
||||
"comment-json": "^4.2.3",
|
||||
"concat-stream": "^2.0.0",
|
||||
"conventional-changelog": "^5.1.0",
|
||||
"conventional-changelog-conventionalcommits": "^7.0.2",
|
||||
@@ -115,7 +116,7 @@
|
||||
"lint-staged": "^14.0.1",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "^9",
|
||||
"next": "14.1.1-canary.26",
|
||||
"next": "14.1.2",
|
||||
"node-mocks-http": "^1.14.1",
|
||||
"nodemon": "3.0.3",
|
||||
"pino": "8.15.0",
|
||||
@@ -128,7 +129,7 @@
|
||||
"read-stream": "^2.1.1",
|
||||
"rimraf": "3.0.2",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "0.33.2",
|
||||
"sharp": "0.32.6",
|
||||
"shelljs": "0.8.5",
|
||||
"simple-git": "^3.20.0",
|
||||
"slash": "3.0.0",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc",
|
||||
"copyfiles": "copyfiles -u 4 \"../next/src/app/(payload)/**\" \"dist/app\"",
|
||||
"copyfiles": "copyfiles -u 2 \"../../app/(payload)/**\" \"dist\"",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"test": "jest",
|
||||
@@ -23,7 +23,9 @@
|
||||
"arg": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"comment-json": "^4.2.3",
|
||||
"degit": "^2.8.4",
|
||||
"detect-package-manager": "^3.0.1",
|
||||
"execa": "^5.0.0",
|
||||
"figures": "^3.2.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
|
||||
@@ -1,16 +1,88 @@
|
||||
import type { CompilerOptions } from 'typescript'
|
||||
|
||||
import chalk from 'chalk'
|
||||
import * as CommentJson from 'comment-json'
|
||||
import { detect } from 'detect-package-manager'
|
||||
import execa from 'execa'
|
||||
import fs from 'fs'
|
||||
import fse from 'fs-extra'
|
||||
import globby from 'globby'
|
||||
import path from 'path'
|
||||
|
||||
import type { CliArgs } from '../types'
|
||||
|
||||
import { copyRecursiveSync } from '../utils/copy-recursive-sync'
|
||||
import { error, info, debug as origDebug, success } from '../utils/log'
|
||||
import { error, info, debug as origDebug, success, warning } from '../utils/log'
|
||||
|
||||
export async function initNext(
|
||||
args: Pick<CliArgs, '--debug'> & { nextDir?: string; useDistFiles?: boolean },
|
||||
): Promise<{ success: boolean }> {
|
||||
const { '--debug': debug, nextDir, useDistFiles } = args
|
||||
type InitNextArgs = Pick<CliArgs, '--debug'> & {
|
||||
projectDir?: string
|
||||
useDistFiles?: boolean
|
||||
}
|
||||
type InitNextResult = { reason?: string; success: boolean; userAppDir?: string }
|
||||
|
||||
export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
|
||||
args.projectDir = args.projectDir || process.cwd()
|
||||
const { projectDir } = args
|
||||
const templateResult = await applyPayloadTemplateFiles(args)
|
||||
if (!templateResult.success) return templateResult
|
||||
|
||||
const { success: installSuccess } = await installDeps(projectDir)
|
||||
if (!installSuccess) {
|
||||
return { ...templateResult, reason: 'Failed to install dependencies', success: false }
|
||||
}
|
||||
|
||||
// Create or find payload.config.ts
|
||||
const createConfigResult = findOrCreatePayloadConfig(projectDir)
|
||||
if (!createConfigResult.success) {
|
||||
return { ...templateResult, ...createConfigResult }
|
||||
}
|
||||
|
||||
// Add `@payload-config` to tsconfig.json `paths`
|
||||
await addPayloadConfigToTsConfig(projectDir)
|
||||
|
||||
// Output directions for user to update next.config.js
|
||||
const withPayloadMessage = `
|
||||
|
||||
${chalk.bold(`Wrap your existing next.config.js with the withPayload function. Here is an example:`)}
|
||||
|
||||
const { withPayload } = require("@payloadcms/next");
|
||||
|
||||
const nextConfig = {
|
||||
// Your Next.js config
|
||||
};
|
||||
|
||||
module.exports = withPayload(nextConfig);
|
||||
|
||||
`
|
||||
|
||||
console.log(withPayloadMessage)
|
||||
|
||||
return templateResult
|
||||
}
|
||||
|
||||
async function addPayloadConfigToTsConfig(projectDir: string) {
|
||||
const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
|
||||
const userTsConfigContent = await fse.readFile(tsConfigPath, {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
const userTsConfig = CommentJson.parse(userTsConfigContent) as {
|
||||
compilerOptions?: CompilerOptions
|
||||
}
|
||||
if (!userTsConfig.compilerOptions && !('extends' in userTsConfig)) {
|
||||
userTsConfig.compilerOptions = {}
|
||||
}
|
||||
|
||||
if (!userTsConfig.compilerOptions.paths?.['@payload-config']) {
|
||||
userTsConfig.compilerOptions.paths = {
|
||||
...(userTsConfig.compilerOptions.paths || {}),
|
||||
'@payload-config': ['./payload.config.ts'],
|
||||
}
|
||||
await fse.writeFile(tsConfigPath, CommentJson.stringify(userTsConfig, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
async function applyPayloadTemplateFiles(args: InitNextArgs): Promise<InitNextResult> {
|
||||
const { '--debug': debug, projectDir, useDistFiles } = args
|
||||
|
||||
info('Initializing Payload app in Next.js project', 1)
|
||||
|
||||
@@ -18,24 +90,18 @@ export async function initNext(
|
||||
if (debug) origDebug(message)
|
||||
}
|
||||
|
||||
let projectDir = process.cwd()
|
||||
if (nextDir) {
|
||||
projectDir = path.resolve(projectDir, nextDir)
|
||||
if (debug) logDebug(`Overriding project directory to ${projectDir}`)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(projectDir)) {
|
||||
error(`Could not find specified project directory at ${projectDir}`)
|
||||
return { success: false }
|
||||
return { reason: `Could not find specified project directory at ${projectDir}`, success: false }
|
||||
}
|
||||
|
||||
// Next.js configs can be next.config.js, next.config.mjs, etc.
|
||||
const foundConfig = (await globby('next.config.*js', { cwd: projectDir }))?.[0]
|
||||
const nextConfigPath = path.resolve(projectDir, foundConfig)
|
||||
if (!fs.existsSync(nextConfigPath)) {
|
||||
error(
|
||||
`No next.config.js found at ${nextConfigPath}. Ensure you are in a Next.js project directory.`,
|
||||
)
|
||||
return { success: false }
|
||||
return {
|
||||
reason: `No next.config.js found at ${nextConfigPath}. Ensure you are in a Next.js project directory.`,
|
||||
success: false,
|
||||
}
|
||||
} else {
|
||||
if (debug) logDebug(`Found Next config at ${nextConfigPath}`)
|
||||
}
|
||||
@@ -43,21 +109,27 @@ export async function initNext(
|
||||
const templateFilesPath =
|
||||
__dirname.endsWith('dist') || useDistFiles
|
||||
? path.resolve(__dirname, '../..', 'dist/app')
|
||||
: path.resolve(__dirname, '../../../next/src/app')
|
||||
: path.resolve(__dirname, '../../../../app')
|
||||
|
||||
if (debug) logDebug(`Using template files from: ${templateFilesPath}`)
|
||||
|
||||
if (!fs.existsSync(templateFilesPath)) {
|
||||
error(`Could not find template source files from ${templateFilesPath}`)
|
||||
return { success: false }
|
||||
return {
|
||||
reason: `Could not find template source files from ${templateFilesPath}`,
|
||||
success: false,
|
||||
}
|
||||
} else {
|
||||
if (debug) logDebug('Found template source files')
|
||||
}
|
||||
|
||||
const userAppDir = path.resolve(projectDir, 'src/app')
|
||||
// src/app or app
|
||||
const userAppDirGlob = await globby(['**/app'], {
|
||||
cwd: projectDir,
|
||||
onlyDirectories: true,
|
||||
})
|
||||
const userAppDir = path.resolve(projectDir, userAppDirGlob?.[0])
|
||||
if (!fs.existsSync(userAppDir)) {
|
||||
error(`Could not find user app directory at ${userAppDir}`)
|
||||
return { success: false }
|
||||
return { reason: `Could not find user app directory inside ${projectDir}`, success: false }
|
||||
} else {
|
||||
logDebug(`Found user app directory: ${userAppDir}`)
|
||||
}
|
||||
@@ -65,5 +137,83 @@ export async function initNext(
|
||||
logDebug(`Copying template files from ${templateFilesPath} to ${userAppDir}`)
|
||||
copyRecursiveSync(templateFilesPath, userAppDir, debug)
|
||||
success('Successfully initialized.')
|
||||
return { success: true }
|
||||
return { success: true, userAppDir }
|
||||
}
|
||||
|
||||
async function installDeps(projectDir: string) {
|
||||
const packageManager = await detect({ cwd: projectDir })
|
||||
if (!packageManager) {
|
||||
throw new Error('Could not detect package manager')
|
||||
}
|
||||
|
||||
info(`Installing dependencies with ${packageManager}`, 1)
|
||||
const packagesToInstall = [
|
||||
'payload',
|
||||
'@payloadcms/db-mongodb',
|
||||
'@payloadcms/next',
|
||||
'@payloadcms/richtext-slate',
|
||||
'@payloadcms/ui',
|
||||
].map((pkg) => `${pkg}@alpha`)
|
||||
|
||||
let exitCode = 0
|
||||
switch (packageManager) {
|
||||
case 'npm': {
|
||||
;({ exitCode } = await execa('npm', ['install', '--save', ...packagesToInstall], {
|
||||
cwd: projectDir,
|
||||
}))
|
||||
break
|
||||
}
|
||||
case 'yarn':
|
||||
case 'pnpm': {
|
||||
;({ exitCode } = await execa(packageManager, ['add', ...packagesToInstall], {
|
||||
cwd: projectDir,
|
||||
}))
|
||||
break
|
||||
}
|
||||
case 'bun': {
|
||||
warning('Bun support is untested.')
|
||||
;({ exitCode } = await execa('bun', ['add', ...packagesToInstall], { cwd: projectDir }))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (exitCode !== 0) {
|
||||
error(`Failed to install dependencies with ${packageManager}`)
|
||||
} else {
|
||||
success(`Successfully installed dependencies`)
|
||||
}
|
||||
return { success: exitCode === 0 }
|
||||
}
|
||||
function findOrCreatePayloadConfig(projectDir: string) {
|
||||
const configPath = path.resolve(projectDir, 'payload.config.ts')
|
||||
if (fs.existsSync(configPath)) {
|
||||
return { message: 'Found existing payload.config.ts', success: true }
|
||||
} else {
|
||||
// Create default config
|
||||
// TODO: Pull this from templates
|
||||
const defaultConfig = `import path from "path";
|
||||
|
||||
import { mongooseAdapter } from "@payloadcms/db-mongodb"; // database-adapter-import
|
||||
import { slateEditor } from "@payloadcms/richtext-slate"; // editor-import
|
||||
import { buildConfig } from "payload/config";
|
||||
|
||||
export default buildConfig({
|
||||
editor: slateEditor({}), // editor-config
|
||||
collections: [],
|
||||
secret: "asdfasdf",
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, "payload-types.ts"),
|
||||
},
|
||||
graphQL: {
|
||||
schemaOutputFile: path.resolve(__dirname, "generated-schema.graphql"),
|
||||
},
|
||||
db: mongooseAdapter({
|
||||
url: "mongodb://localhost:27017/next-payload-3",
|
||||
}),
|
||||
});
|
||||
`
|
||||
|
||||
fse.writeFileSync(configPath, defaultConfig)
|
||||
return { message: 'Created default payload.config.ts', success: true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { parseTemplate } from './lib/parse-template'
|
||||
import { selectDb } from './lib/select-db'
|
||||
import { getValidTemplates, validateTemplate } from './lib/templates'
|
||||
import { writeEnvFile } from './lib/write-env-file'
|
||||
import { success } from './utils/log'
|
||||
import { error, success } from './utils/log'
|
||||
import { helpMessage, successMessage, welcomeMessage } from './utils/messages'
|
||||
|
||||
export class Main {
|
||||
@@ -61,6 +61,11 @@ export class Main {
|
||||
|
||||
if (this.args['--init-next']) {
|
||||
const result = await initNext(this.args)
|
||||
if (!result.success) {
|
||||
error(result.reason || 'Failed to initialize Payload app in Next.js project')
|
||||
} else {
|
||||
success('Payload app successfully initialized in Next.js project')
|
||||
}
|
||||
process.exit(result.success ? 0 : 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"description": "The officially supported MongoDB database adapter for Payload - Update 2",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -6,7 +6,14 @@ import mongoose from 'mongoose'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
export const connect: Connect = async function connect(this: MongooseAdapter) {
|
||||
export const connect: Connect = async function connect(
|
||||
this: MongooseAdapter,
|
||||
options = {
|
||||
hotReload: false,
|
||||
},
|
||||
) {
|
||||
const { hotReload } = options
|
||||
|
||||
if (this.url === false) {
|
||||
return
|
||||
}
|
||||
@@ -24,6 +31,8 @@ export const connect: Connect = async function connect(this: MongooseAdapter) {
|
||||
useFacet: undefined,
|
||||
}
|
||||
|
||||
if (hotReload) connectionOptions.autoIndex = false
|
||||
|
||||
try {
|
||||
this.connection = (await mongoose.connect(urlToConnect, connectionOptions)).connection
|
||||
|
||||
@@ -34,12 +43,15 @@ export const connect: Connect = async function connect(this: MongooseAdapter) {
|
||||
this.beginTransaction = undefined
|
||||
}
|
||||
|
||||
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
|
||||
this.payload.logger.info('---- DROPPING DATABASE ----')
|
||||
await mongoose.connection.dropDatabase()
|
||||
this.payload.logger.info('---- DROPPED DATABASE ----')
|
||||
if (!hotReload) {
|
||||
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
|
||||
this.payload.logger.info('---- DROPPING DATABASE ----')
|
||||
await mongoose.connection.dropDatabase()
|
||||
this.payload.logger.info('---- DROPPED DATABASE ----')
|
||||
}
|
||||
|
||||
this.payload.logger.info(successfulConnectionMessage)
|
||||
}
|
||||
this.payload.logger.info(successfulConnectionMessage)
|
||||
} catch (err) {
|
||||
this.payload.logger.error(`Error: cannot connect to MongoDB. Details: ${err.message}`, err)
|
||||
process.exit(1)
|
||||
|
||||
@@ -10,6 +10,7 @@ export const destroy: Destroy = async function destroy(this: MongooseAdapter) {
|
||||
await mongoose.connection.close()
|
||||
await this.mongoMemoryServer.stop()
|
||||
} else {
|
||||
await mongoose.connection.close()
|
||||
await mongoose.disconnect()
|
||||
}
|
||||
Object.keys(mongoose.models).map((model) => mongoose.deleteModel(model))
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { buildGlobalModel } from './models/buildGlobalModel'
|
||||
import buildSchema from './models/buildSchema'
|
||||
import getBuildQueryPlugin from './queries/buildQuery'
|
||||
|
||||
export const init: Init = async function init(this: MongooseAdapter) {
|
||||
export const init: Init = function init(this: MongooseAdapter) {
|
||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
||||
const schema = buildCollectionSchema(collection, this.payload.config)
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable class-methods-use-this */
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
/* eslint-disable no-use-before-define */
|
||||
import type { IndexOptions, SchemaOptions, SchemaTypeOptions } from 'mongoose'
|
||||
import type { IndexOptions, Schema, SchemaOptions, SchemaTypeOptions } from 'mongoose'
|
||||
import type { SanitizedConfig, SanitizedLocalizationConfig } from 'payload/config'
|
||||
import type {
|
||||
ArrayField,
|
||||
@@ -32,7 +31,7 @@ import type {
|
||||
UploadField,
|
||||
} from 'payload/types'
|
||||
|
||||
import { Schema } from 'mongoose'
|
||||
import mongoose from 'mongoose'
|
||||
import {
|
||||
fieldAffectsData,
|
||||
fieldIsLocalized,
|
||||
@@ -126,7 +125,7 @@ const buildSchema = (
|
||||
}
|
||||
}
|
||||
|
||||
const schema = new Schema(fields, options)
|
||||
const schema = new mongoose.Schema(fields, options)
|
||||
|
||||
schemaFields.forEach((field) => {
|
||||
if (!fieldIsPresentationalOnly(field)) {
|
||||
@@ -176,7 +175,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const fieldSchema = {
|
||||
type: [new Schema({}, { _id: false, discriminatorKey: 'blockType' })],
|
||||
type: [new mongoose.Schema({}, { _id: false, discriminatorKey: 'blockType' })],
|
||||
default: undefined,
|
||||
}
|
||||
|
||||
@@ -185,7 +184,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
})
|
||||
|
||||
field.blocks.forEach((blockItem: Block) => {
|
||||
const blockSchema = new Schema({}, { _id: false, id: false })
|
||||
const blockSchema = new mongoose.Schema({}, { _id: false, id: false })
|
||||
|
||||
blockItem.fields.forEach((blockField) => {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]
|
||||
@@ -307,7 +306,10 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
config: SanitizedConfig,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed }
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
@@ -405,17 +407,17 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
_id: false,
|
||||
type: Schema.Types.Mixed,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
value: {
|
||||
type: Schema.Types.Mixed,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
refPath: `${field.name}.${locale}.relationTo`,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: Schema.Types.Mixed,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
}
|
||||
@@ -431,10 +433,10 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
_id: false,
|
||||
type: Schema.Types.Mixed,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
value: {
|
||||
type: Schema.Types.Mixed,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
refPath: `${field.name}.relationTo`,
|
||||
},
|
||||
}
|
||||
@@ -448,7 +450,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
} else {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: Schema.Types.Mixed,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
|
||||
@@ -470,7 +472,10 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
config: SanitizedConfig,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed }
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
@@ -586,7 +591,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: Schema.Types.Mixed,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.7.0",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -46,7 +46,14 @@ const connectWithReconnect = async function ({
|
||||
})
|
||||
}
|
||||
|
||||
export const connect: Connect = async function connect(this: PostgresAdapter, payload) {
|
||||
export const connect: Connect = async function connect(
|
||||
this: PostgresAdapter,
|
||||
options = {
|
||||
hotReload: false,
|
||||
},
|
||||
) {
|
||||
const { hotReload } = options
|
||||
|
||||
this.schema = {
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
@@ -55,23 +62,26 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
|
||||
|
||||
try {
|
||||
this.pool = new Pool(this.poolOptions)
|
||||
await connectWithReconnect({ adapter: this, payload })
|
||||
await connectWithReconnect({ adapter: this, payload: this.payload })
|
||||
|
||||
const logger = this.logger || false
|
||||
|
||||
this.drizzle = drizzle(this.pool, { logger, schema: this.schema })
|
||||
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
|
||||
this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)
|
||||
await this.drizzle.execute(
|
||||
sql.raw(`
|
||||
drop schema if exists ${this.schemaName || 'public'} cascade;
|
||||
create schema ${this.schemaName || 'public'};
|
||||
`),
|
||||
)
|
||||
this.payload.logger.info('---- DROPPED TABLES ----')
|
||||
|
||||
if (!hotReload) {
|
||||
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
|
||||
this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)
|
||||
await this.drizzle.execute(
|
||||
sql.raw(`
|
||||
drop schema if exists ${this.schemaName || 'public'} cascade;
|
||||
create schema ${this.schemaName || 'public'};
|
||||
`),
|
||||
)
|
||||
this.payload.logger.info('---- DROPPED TABLES ----')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
payload.logger.error(`Error: cannot connect to Postgres. Details: ${err.message}`, err)
|
||||
this.payload.logger.error(`Error: cannot connect to Postgres. Details: ${err.message}`, err)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.d.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.d.ts",
|
||||
"bin": {
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build:webpack": "webpack --config webpack.config.js",
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:webpack && pnpm build:types",
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
@@ -49,6 +49,7 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/react": "18.2.15",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@types/ws": "^8.5.10",
|
||||
"css-loader": "^6.10.0",
|
||||
"css-minimizer-webpack-plugin": "^6.0.0",
|
||||
"file-loader": "6.2.0",
|
||||
@@ -78,7 +79,8 @@
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"react-diff-viewer-continued": "3.2.6",
|
||||
"react-toastify": "8.2.0",
|
||||
"sass": "^1.71.1"
|
||||
"sass": "^1.71.1",
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"http-status": "1.6.2",
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { withPayload } from '../withPayload'
|
||||
export { default as withPayload } from '../withPayload'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export { AdminLayout } from './layouts/Admin'
|
||||
export { DocumentLayout } from './layouts/Document'
|
||||
export { RootLayout } from './layouts/Root'
|
||||
export { Dashboard as DashboardPage } from './views/Dashboard'
|
||||
export { Login } from './views/Login'
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { SanitizedConfig } from 'payload/types'
|
||||
|
||||
import { DocumentHeader } from '@payloadcms/ui'
|
||||
import '@payloadcms/ui/scss/app.scss'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { initPage } from '../../utilities/initPage'
|
||||
|
||||
export const metadata = {
|
||||
description: 'Generated by Next.js',
|
||||
title: 'Next.js',
|
||||
}
|
||||
|
||||
export const DocumentLayout = async ({
|
||||
children,
|
||||
collectionSlug,
|
||||
config: configPromise,
|
||||
globalSlug,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
collectionSlug?: string
|
||||
config: Promise<SanitizedConfig>
|
||||
globalSlug?: string
|
||||
}) => {
|
||||
const { collectionConfig, globalConfig, req } = await initPage({
|
||||
collectionSlug,
|
||||
config: configPromise,
|
||||
globalSlug,
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<DocumentHeader
|
||||
collectionConfig={collectionConfig}
|
||||
config={req.payload.config}
|
||||
globalConfig={globalConfig}
|
||||
i18n={req.i18n}
|
||||
/>
|
||||
{children}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -5,15 +5,17 @@ import { translations } from '@payloadcms/translations/client'
|
||||
import { RootProvider, buildComponentMap } from '@payloadcms/ui'
|
||||
import '@payloadcms/ui/scss/app.scss'
|
||||
import { headers as getHeaders } from 'next/headers'
|
||||
import { parseCookies } from 'payload/auth'
|
||||
import { createClientConfig } from 'payload/config'
|
||||
import { deepMerge } from 'payload/utilities'
|
||||
import React from 'react'
|
||||
import 'react-toastify/dist/ReactToastify.css'
|
||||
|
||||
import { getRequestLanguage } from '../../utilities/getRequestLanguage'
|
||||
import { DefaultEditView } from '../../views/Edit/Default'
|
||||
import { DefaultListView } from '../../views/List/Default'
|
||||
import { DefaultCell } from '../../views/List/Default/Cell'
|
||||
import { getRequestLanguage } from '../../utilities/getRequestLanguage'
|
||||
import { getPayload } from '../../utilities/getPayload'
|
||||
|
||||
export const metadata = {
|
||||
description: 'Generated by Next.js',
|
||||
@@ -34,10 +36,13 @@ export const RootLayout = async ({
|
||||
|
||||
const headers = getHeaders()
|
||||
|
||||
const { cookies, user } = await auth({
|
||||
config: configPromise,
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const { cookies, user, permissions } = await auth({
|
||||
payload,
|
||||
headers,
|
||||
})
|
||||
|
||||
const lang =
|
||||
getRequestLanguage({
|
||||
cookies,
|
||||
@@ -58,6 +63,7 @@ export const RootLayout = async ({
|
||||
DefaultEditView,
|
||||
DefaultListView,
|
||||
config,
|
||||
permissions: permissions,
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -51,6 +51,10 @@ if (!cached) {
|
||||
}
|
||||
|
||||
export const getGraphql = async (config: Promise<SanitizedConfig> | SanitizedConfig) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
cached = global._payload_graphql = { graphql: null, promise: null }
|
||||
}
|
||||
|
||||
if (cached.graphql) {
|
||||
return cached.graphql
|
||||
}
|
||||
@@ -110,7 +114,7 @@ export const POST =
|
||||
}
|
||||
return response
|
||||
},
|
||||
schema: schema,
|
||||
schema,
|
||||
validationRules: (request, args, defaultRules) => defaultRules.concat(validationRules(args)),
|
||||
})(originalRequest)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ if (!cached) {
|
||||
}
|
||||
|
||||
export const getFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap => {
|
||||
if (cached) {
|
||||
if (cached && process.env.NODE_ENV !== 'development') {
|
||||
return cached
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { CollectionRouteHandler } from '../types'
|
||||
|
||||
export const find: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { searchParams } = req
|
||||
|
||||
// parse using `qs` to handle `where` queries
|
||||
const { depth, draft, limit, page, sort, where } = qs.parse(searchParams.toString()) as {
|
||||
depth?: string
|
||||
|
||||
@@ -208,6 +208,7 @@ export const GET =
|
||||
if (`doc-${slug2}-by-id` in endpoints.collection.GET) {
|
||||
// /:collection/access/:id
|
||||
// /:collection/versions/:id
|
||||
|
||||
res = await (
|
||||
endpoints.collection.GET[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID
|
||||
)({ id: slug3, collection, req })
|
||||
@@ -334,6 +335,7 @@ export const POST =
|
||||
// /:collection/forgot-password
|
||||
// /:collection/reset-password
|
||||
// /:collection/refresh-token
|
||||
|
||||
res = await (endpoints.collection.POST?.[slug2] as CollectionRouteHandler)({
|
||||
collection,
|
||||
req,
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import type { PayloadRequest, SanitizedConfig } from 'payload/types'
|
||||
import type { Payload, PayloadRequest } from 'payload/types'
|
||||
|
||||
import { getPayload } from 'payload'
|
||||
import { getAccessResults, getAuthenticatedUser, parseCookies } from 'payload/auth'
|
||||
import { cache } from 'react'
|
||||
|
||||
export const auth = cache(
|
||||
async ({
|
||||
config,
|
||||
headers,
|
||||
}: {
|
||||
config: Promise<SanitizedConfig> | SanitizedConfig
|
||||
headers: Request['headers']
|
||||
}) => {
|
||||
async ({ headers, payload }: { headers: Request['headers']; payload: Payload }) => {
|
||||
const cookies = parseCookies(headers)
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
const user = await getAuthenticatedUser({
|
||||
cookies,
|
||||
headers,
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import type { ClientConfig, Field, SanitizedConfig } from 'payload/types'
|
||||
|
||||
export const sanitizeField = (f) => {
|
||||
const field = { ...f }
|
||||
|
||||
if ('access' in field) delete field.access
|
||||
if ('hooks' in field) delete field.hooks
|
||||
if ('validate' in field) delete field.validate
|
||||
if ('defaultValue' in field) delete field.defaultValue
|
||||
if ('label' in field) delete field.label
|
||||
|
||||
if ('fields' in field) {
|
||||
field.fields = sanitizeFields(field.fields)
|
||||
}
|
||||
|
||||
if ('editor' in field) {
|
||||
delete field.editor
|
||||
}
|
||||
|
||||
if ('blocks' in field) {
|
||||
field.blocks = field.blocks.map((block) => {
|
||||
const sanitized = { ...block }
|
||||
sanitized.fields = sanitizeFields(sanitized.fields)
|
||||
return sanitized
|
||||
})
|
||||
}
|
||||
|
||||
if ('tabs' in field) {
|
||||
field.tabs = field.tabs.map((tab) => sanitizeField(tab))
|
||||
}
|
||||
|
||||
if ('admin' in field) {
|
||||
field.admin = { ...field.admin }
|
||||
|
||||
if ('components' in field.admin) {
|
||||
delete field.admin.components
|
||||
}
|
||||
|
||||
if ('condition' in field.admin) {
|
||||
delete field.admin.condition
|
||||
}
|
||||
|
||||
if ('description' in field.admin) {
|
||||
delete field.admin.description
|
||||
}
|
||||
}
|
||||
|
||||
return field
|
||||
}
|
||||
|
||||
const sanitizeCollections = (
|
||||
collections: SanitizedConfig['collections'],
|
||||
): ClientConfig['collections'] =>
|
||||
collections.map((collection) => {
|
||||
const sanitized = { ...collection }
|
||||
sanitized.fields = sanitizeFields(sanitized.fields)
|
||||
delete sanitized.hooks
|
||||
delete sanitized.access
|
||||
delete sanitized.endpoints
|
||||
|
||||
if ('editor' in sanitized) delete sanitized.editor
|
||||
|
||||
if ('admin' in sanitized) {
|
||||
sanitized.admin = { ...sanitized.admin }
|
||||
|
||||
if ('components' in sanitized.admin) {
|
||||
delete sanitized.admin.components
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized
|
||||
})
|
||||
|
||||
const sanitizeGlobals = (globals: SanitizedConfig['globals']): ClientConfig['globals'] =>
|
||||
globals.map((global) => {
|
||||
const sanitized = { ...global }
|
||||
sanitized.fields = sanitizeFields(sanitized.fields)
|
||||
delete sanitized.hooks
|
||||
delete sanitized.access
|
||||
delete sanitized.endpoints
|
||||
|
||||
if ('admin' in sanitized) {
|
||||
sanitized.admin = { ...sanitized.admin }
|
||||
|
||||
if ('components' in sanitized.admin) {
|
||||
delete sanitized.admin.components
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized
|
||||
})
|
||||
|
||||
export const sanitizeFields = (fields: Field[]): Field[] => fields.map(sanitizeField)
|
||||
|
||||
export const createClientConfig = async (
|
||||
configPromise: Promise<SanitizedConfig> | SanitizedConfig,
|
||||
): Promise<ClientConfig> => {
|
||||
const config = await configPromise
|
||||
const clientConfig = { ...config }
|
||||
|
||||
delete clientConfig.endpoints
|
||||
delete clientConfig.db
|
||||
delete clientConfig.editor
|
||||
|
||||
'localization' in clientConfig &&
|
||||
clientConfig.localization &&
|
||||
clientConfig.localization.locales.forEach((locale) => {
|
||||
delete locale.toString
|
||||
})
|
||||
|
||||
clientConfig.onInit = undefined
|
||||
|
||||
clientConfig.collections = sanitizeCollections(clientConfig.collections)
|
||||
clientConfig.globals = sanitizeGlobals(clientConfig.globals)
|
||||
|
||||
return clientConfig
|
||||
}
|
||||
@@ -7,13 +7,13 @@ import type {
|
||||
|
||||
import { initI18n } from '@payloadcms/translations'
|
||||
import { translations } from '@payloadcms/translations/api'
|
||||
import { getPayload } from 'payload'
|
||||
import { getAuthenticatedUser } from 'payload/auth'
|
||||
import { parseCookies } from 'payload/auth'
|
||||
import { getDataLoader } from 'payload/utilities'
|
||||
import { URL } from 'url'
|
||||
|
||||
import { getDataAndFile } from './getDataAndFile'
|
||||
import { getPayload } from './getPayload'
|
||||
import { getRequestLanguage } from './getRequestLanguage'
|
||||
import { getRequestLocales } from './getRequestLocales'
|
||||
|
||||
@@ -32,6 +32,7 @@ export const createPayloadRequest = async ({
|
||||
}: Args): Promise<PayloadRequest> => {
|
||||
const cookies = parseCookies(request.headers)
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const { collections, config } = payload
|
||||
|
||||
let collection: Collection = undefined
|
||||
|
||||
71
packages/next/src/utilities/getPayload.ts
Normal file
71
packages/next/src/utilities/getPayload.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { GeneratedTypes, Payload } from 'payload'
|
||||
import type { InitOptions } from 'payload/config'
|
||||
|
||||
import { BasePayload } from 'payload'
|
||||
import WebSocket from 'ws'
|
||||
|
||||
let cached = global._payload
|
||||
|
||||
if (!cached) {
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
cached = global._payload = { payload: null, promise: null }
|
||||
}
|
||||
|
||||
export const getPayload = async (options: InitOptions): Promise<Payload> => {
|
||||
if (cached.payload) {
|
||||
const config = await options.config
|
||||
|
||||
if (cached.reload) {
|
||||
cached.reload = false
|
||||
if (typeof cached.payload.db.destroy === 'function') {
|
||||
await cached.payload.db.destroy()
|
||||
}
|
||||
|
||||
cached.payload.config = config
|
||||
|
||||
cached.payload.collections = config.collections.reduce((collections, collection) => {
|
||||
collections[collection.slug] = { config: collection }
|
||||
return collections
|
||||
}, {})
|
||||
|
||||
// TODO: re-build payload.globals as well as any other properties
|
||||
// that may change on Payload singleton
|
||||
|
||||
await cached.payload.db.init()
|
||||
await cached.payload.db.connect({ hotReload: true })
|
||||
}
|
||||
|
||||
return cached.payload
|
||||
}
|
||||
|
||||
if (!cached.promise) {
|
||||
cached.promise = new BasePayload<GeneratedTypes>().init(options)
|
||||
}
|
||||
|
||||
try {
|
||||
cached.payload = await cached.promise
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
try {
|
||||
const ws = new WebSocket('ws://localhost:3000/_next/webpack-hmr')
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (typeof event.data === 'string') {
|
||||
const data = JSON.parse(event.data)
|
||||
|
||||
if ('action' in data && data.action === 'serverComponentChanges') {
|
||||
cached.reload = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// swallow e
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
cached.promise = null
|
||||
throw e
|
||||
}
|
||||
|
||||
return cached.payload
|
||||
}
|
||||
@@ -11,18 +11,15 @@ import { translations } from '@payloadcms/translations/client'
|
||||
import { findLocaleFromCode } from '@payloadcms/ui'
|
||||
import { headers as getHeaders } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
import { createLocalReq } from 'payload/utilities'
|
||||
import qs from 'qs'
|
||||
|
||||
import { getPayload } from '../utilities/getPayload'
|
||||
import { auth } from './auth'
|
||||
import { getRequestLanguage } from './getRequestLanguage'
|
||||
|
||||
type Args = {
|
||||
collectionSlug?: string
|
||||
config: Promise<SanitizedConfig> | SanitizedConfig
|
||||
globalSlug?: string
|
||||
localeParam?: string
|
||||
redirectUnauthenticatedUser?: boolean
|
||||
route?: string
|
||||
searchParams?: { [key: string]: string | string[] | undefined }
|
||||
@@ -30,26 +27,28 @@ type Args = {
|
||||
|
||||
export const initPage = async ({
|
||||
config: configPromise,
|
||||
localeParam,
|
||||
redirectUnauthenticatedUser = false,
|
||||
route,
|
||||
searchParams,
|
||||
}: Args): Promise<InitPageResult> => {
|
||||
const headers = getHeaders()
|
||||
const localeParam = searchParams?.locale as string
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const { cookies, permissions, user } = await auth({
|
||||
config: configPromise,
|
||||
headers,
|
||||
payload,
|
||||
})
|
||||
|
||||
const config = await configPromise
|
||||
const routeSegments = route.replace(config.routes.admin, '').split('/').filter(Boolean)
|
||||
const routeSegments = route.replace(payload.config.routes.admin, '').split('/').filter(Boolean)
|
||||
const collectionSlug = routeSegments[0] === 'collections' ? routeSegments[1] : undefined
|
||||
const globalSlug = routeSegments[0] === 'globals' ? routeSegments[1] : undefined
|
||||
|
||||
const { collections, globals, localization, routes } = config
|
||||
const { collections, globals, localization, routes } = payload.config
|
||||
|
||||
if (redirectUnauthenticatedUser && !user && route !== '/login') {
|
||||
if ('redirect' in searchParams) delete searchParams.redirect
|
||||
|
||||
const stringifiedSearchParams = Object.keys(searchParams ?? {}).length
|
||||
? `?${qs.stringify(searchParams)}`
|
||||
: ''
|
||||
@@ -57,15 +56,14 @@ export const initPage = async ({
|
||||
redirect(`${routes.admin}/login?redirect=${route + stringifiedSearchParams}`)
|
||||
}
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
const defaultLocale =
|
||||
localization && localization.defaultLocale ? localization.defaultLocale : 'en'
|
||||
const localeCode = localeParam || defaultLocale
|
||||
const locale = localization && findLocaleFromCode(localization, localeCode)
|
||||
const language = getRequestLanguage({ cookies, headers })
|
||||
|
||||
const i18n = initI18n({
|
||||
config: config.i18n,
|
||||
config: payload.config.i18n,
|
||||
context: 'client',
|
||||
language,
|
||||
translations,
|
||||
@@ -96,9 +94,11 @@ export const initPage = async ({
|
||||
|
||||
return {
|
||||
collectionConfig,
|
||||
cookies,
|
||||
globalConfig,
|
||||
locale,
|
||||
permissions,
|
||||
req,
|
||||
translations: i18n.translations,
|
||||
}
|
||||
}
|
||||
|
||||
5
packages/next/src/utilities/timestamp.ts
Normal file
5
packages/next/src/utilities/timestamp.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const timestamp = (label: string) => {
|
||||
if (!process.env.PAYLOAD_TIME) process.env.PAYLOAD_TIME = String(new Date().getTime())
|
||||
const now = new Date()
|
||||
console.log(`[${now.getTime() - Number(process.env.PAYLOAD_TIME)}ms] ${label}`)
|
||||
}
|
||||
@@ -14,9 +14,9 @@ const chars = {
|
||||
const baseClass = 'query-inspector'
|
||||
|
||||
const Bracket = ({
|
||||
type,
|
||||
comma = false,
|
||||
position,
|
||||
type,
|
||||
}: {
|
||||
comma?: boolean
|
||||
position: 'end' | 'start'
|
||||
@@ -49,7 +49,7 @@ export const RenderJSON = ({
|
||||
parentType = 'object',
|
||||
trailingComma = false,
|
||||
}: Args) => {
|
||||
const objectKeys = Object.keys(object)
|
||||
const objectKeys = object ? Object.keys(object) : []
|
||||
const objectLength = objectKeys.length
|
||||
const [isOpen, setIsOpen] = React.useState<boolean>(true)
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
'use client'
|
||||
import { RenderFields, useComponentMap } from '@payloadcms/ui'
|
||||
import { FieldMap, RenderFields, useComponentMap } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
export const CreateFirstUserFields: React.FC<{
|
||||
userSlug: string
|
||||
}> = ({ userSlug }) => {
|
||||
createFirstUserFieldMap: FieldMap
|
||||
}> = ({ userSlug, createFirstUserFieldMap }) => {
|
||||
const { getFieldMap } = useComponentMap()
|
||||
|
||||
const fieldMap = getFieldMap({ collectionSlug: userSlug })
|
||||
|
||||
return <RenderFields fieldMap={fieldMap} />
|
||||
return (
|
||||
<RenderFields
|
||||
fieldMap={[...(fieldMap || []), ...(createFirstUserFieldMap || [])]}
|
||||
operation="create"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Field } from 'payload/types'
|
||||
|
||||
import { Form, FormSubmit, buildStateFromSchema } from '@payloadcms/ui'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { Form, FormSubmit, buildStateFromSchema, mapFields } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import type { AdminViewProps } from '../Root'
|
||||
@@ -16,6 +15,7 @@ export const CreateFirstUser: React.FC<AdminViewProps> = async ({ initPageResult
|
||||
req,
|
||||
req: {
|
||||
payload: {
|
||||
config,
|
||||
config: {
|
||||
admin: { user: userSlug },
|
||||
routes: { admin: adminRoute, api: apiRoute },
|
||||
@@ -25,21 +25,7 @@ export const CreateFirstUser: React.FC<AdminViewProps> = async ({ initPageResult
|
||||
},
|
||||
} = initPageResult
|
||||
|
||||
if (req.user) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
const { docs } = await req.payload.find({
|
||||
collection: userSlug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
if (docs.length > 0) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
const fields = [
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
@@ -48,17 +34,23 @@ export const CreateFirstUser: React.FC<AdminViewProps> = async ({ initPageResult
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
type: 'text',
|
||||
label: req.t('general:password'),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'confirm-password',
|
||||
type: 'confirmPassword',
|
||||
type: 'text',
|
||||
label: req.t('authentication:confirmPassword'),
|
||||
required: true,
|
||||
},
|
||||
] as Field[]
|
||||
]
|
||||
|
||||
const createFirstUserFieldMap = mapFields({
|
||||
fieldSchema: fields,
|
||||
config,
|
||||
parentPath: userSlug,
|
||||
})
|
||||
|
||||
const formState = await buildStateFromSchema({
|
||||
fieldSchema: fields,
|
||||
@@ -78,7 +70,10 @@ export const CreateFirstUser: React.FC<AdminViewProps> = async ({ initPageResult
|
||||
redirect={adminRoute}
|
||||
validationOperation="create"
|
||||
>
|
||||
<CreateFirstUserFields userSlug={userSlug} />
|
||||
<CreateFirstUserFields
|
||||
userSlug={userSlug}
|
||||
createFirstUserFieldMap={createFirstUserFieldMap}
|
||||
/>
|
||||
<FormSubmit>{req.t('general:create')}</FormSubmit>
|
||||
</Form>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
import { Permissions } from 'payload/auth'
|
||||
|
||||
const baseClass = 'dashboard'
|
||||
|
||||
@@ -22,7 +23,8 @@ export const DefaultDashboardClient: React.FC<{
|
||||
Link: React.ComponentType
|
||||
visibleCollections: string[]
|
||||
visibleGlobals: string[]
|
||||
}> = ({ Link, visibleCollections, visibleGlobals }) => {
|
||||
permissions: Permissions
|
||||
}> = ({ Link, visibleCollections, visibleGlobals, permissions }) => {
|
||||
const config = useConfig()
|
||||
|
||||
const {
|
||||
@@ -31,7 +33,7 @@ export const DefaultDashboardClient: React.FC<{
|
||||
routes: { admin },
|
||||
} = config
|
||||
|
||||
const { permissions, user } = useAuth()
|
||||
const { user } = useAuth()
|
||||
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react'
|
||||
|
||||
import { DefaultDashboardClient } from './index.client'
|
||||
import './index.scss'
|
||||
import { Permissions } from 'payload/auth'
|
||||
|
||||
const baseClass = 'dashboard'
|
||||
|
||||
@@ -13,6 +14,7 @@ export type DashboardProps = {
|
||||
config: SanitizedConfig
|
||||
visibleCollections: string[]
|
||||
visibleGlobals: string[]
|
||||
permissions: Permissions
|
||||
}
|
||||
|
||||
export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
@@ -25,6 +27,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
},
|
||||
visibleCollections,
|
||||
visibleGlobals,
|
||||
permissions,
|
||||
} = props
|
||||
|
||||
return (
|
||||
@@ -38,6 +41,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
Link={Link}
|
||||
visibleCollections={visibleCollections}
|
||||
visibleGlobals={visibleGlobals}
|
||||
permissions={permissions}
|
||||
/>
|
||||
{Array.isArray(afterDashboard) &&
|
||||
afterDashboard.map((Component, i) => <Component key={i} />)}
|
||||
|
||||
@@ -43,6 +43,7 @@ export const Dashboard: React.FC<AdminViewProps> = ({
|
||||
config,
|
||||
visibleCollections,
|
||||
visibleGlobals,
|
||||
permissions,
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -38,8 +38,6 @@ export const getViewsFromConfig = async ({
|
||||
let DefaultView: EditViewComponent = null
|
||||
let CustomView: EditViewComponent = null
|
||||
|
||||
const [entityType, entitySlug, createOrID, tabViewName, segmentFive] = routeSegments
|
||||
|
||||
const views =
|
||||
(collectionConfig && collectionConfig?.admin?.components?.views) ||
|
||||
(globalConfig && globalConfig?.admin?.components?.views)
|
||||
@@ -51,6 +49,9 @@ export const getViewsFromConfig = async ({
|
||||
config?.admin?.livePreview?.globals?.includes(globalConfig?.slug)
|
||||
|
||||
if (collectionConfig) {
|
||||
const [collectionEntity, collectionSlug, createOrID, nestedViewSlug, segmentFive] =
|
||||
routeSegments
|
||||
|
||||
const {
|
||||
admin: { hidden },
|
||||
} = collectionConfig
|
||||
@@ -60,7 +61,7 @@ export const getViewsFromConfig = async ({
|
||||
}
|
||||
|
||||
// `../:id`, or `../create`
|
||||
if (!tabViewName) {
|
||||
if (!nestedViewSlug) {
|
||||
switch (createOrID) {
|
||||
case 'create': {
|
||||
if ('create' in docPermissions && docPermissions?.create?.permission) {
|
||||
@@ -79,10 +80,10 @@ export const getViewsFromConfig = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (tabViewName) {
|
||||
if (nestedViewSlug) {
|
||||
// `../:id/versions/:version`, etc
|
||||
if (segmentFive) {
|
||||
if (tabViewName === 'versions') {
|
||||
if (nestedViewSlug === 'versions') {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Version')
|
||||
DefaultView = DefaultVersionView
|
||||
@@ -92,7 +93,7 @@ export const getViewsFromConfig = async ({
|
||||
|
||||
// `../:id/api`, `../:id/preview`, `../:id/versions`, etc
|
||||
if (routeSegments?.length === 4) {
|
||||
switch (tabViewName) {
|
||||
switch (nestedViewSlug) {
|
||||
case 'api': {
|
||||
if (collectionConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = getCustomViewByKey(views, 'API')
|
||||
@@ -117,7 +118,7 @@ export const getViewsFromConfig = async ({
|
||||
}
|
||||
|
||||
default: {
|
||||
const path = `/${tabViewName}`
|
||||
const path = `/${nestedViewSlug}`
|
||||
CustomView = getCustomViewByPath(views, path)
|
||||
break
|
||||
}
|
||||
@@ -127,6 +128,8 @@ export const getViewsFromConfig = async ({
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
const [globalEntity, globalSlug, nestedViewSlug] = routeSegments
|
||||
|
||||
const {
|
||||
admin: { hidden },
|
||||
} = globalConfig
|
||||
@@ -135,14 +138,14 @@ export const getViewsFromConfig = async ({
|
||||
return null
|
||||
}
|
||||
|
||||
if (!routeSegments?.length) {
|
||||
if (routeSegments?.length === 2) {
|
||||
if (docPermissions?.read?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Default')
|
||||
DefaultView = DefaultEditView
|
||||
}
|
||||
} else if (routeSegments?.length === 1) {
|
||||
} else if (routeSegments?.length === 3) {
|
||||
// `../:slug/api`, `../:slug/preview`, `../:slug/versions`, etc
|
||||
switch (tabViewName) {
|
||||
switch (nestedViewSlug) {
|
||||
case 'api': {
|
||||
if (globalConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = getCustomViewByKey(views, 'API')
|
||||
@@ -176,7 +179,7 @@ export const getViewsFromConfig = async ({
|
||||
}
|
||||
} else if (routeSegments?.length === 2) {
|
||||
// `../:slug/versions/:version`, etc
|
||||
if (tabViewName === 'versions') {
|
||||
if (nestedViewSlug === 'versions') {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Version')
|
||||
DefaultView = DefaultVersionView
|
||||
|
||||
@@ -24,8 +24,9 @@ import queryString from 'qs'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { AdminViewProps } from '../Root'
|
||||
import type { GenerateEditViewMetadata } from './getMetaBySegment'
|
||||
|
||||
import { GenerateEditViewMetadata, getMetaBySegment } from './getMetaBySegment'
|
||||
import { getMetaBySegment } from './getMetaBySegment'
|
||||
import { getViewsFromConfig } from './getViewsFromConfig'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async (args) => getMetaBySegment(args)
|
||||
@@ -58,7 +59,7 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
const segments = Array.isArray(params?.segments) ? params.segments : []
|
||||
const [entityType, entitySlug, createOrID] = segments
|
||||
const collectionSlug = entityType === 'collections' ? entitySlug : undefined
|
||||
const globalSlug = entitySlug === 'globals' ? entitySlug : undefined
|
||||
const globalSlug = entityType === 'globals' ? entitySlug : undefined
|
||||
const isCreating = createOrID === 'create'
|
||||
const id = (collectionSlug && !isCreating && createOrID) || undefined
|
||||
|
||||
@@ -208,7 +209,7 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
/>
|
||||
<HydrateClientUser permissions={permissions} user={user} />
|
||||
<SetDocumentInfo
|
||||
action={action}
|
||||
action={`${action}?${queryString.stringify(formQueryParams)}`}
|
||||
apiURL={apiURL}
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
disableActions={false}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getFormState,
|
||||
useComponentMap,
|
||||
useConfig,
|
||||
useDocumentEvents,
|
||||
useDocumentInfo,
|
||||
} from '@payloadcms/ui'
|
||||
import { Upload } from '@payloadcms/ui/elements'
|
||||
@@ -66,13 +67,15 @@ export const DefaultEditView: React.FC = () => {
|
||||
|
||||
const globalConfig = globalSlug && globals.find((global) => global.slug === globalSlug)
|
||||
|
||||
const [schemaPath] = React.useState(collectionConfig?.slug || globalConfig?.slug)
|
||||
const schemaPath = collectionConfig?.slug || globalConfig?.slug
|
||||
|
||||
const fieldMap = getFieldMap({
|
||||
collectionSlug: collectionConfig?.slug,
|
||||
globalSlug: globalConfig?.slug,
|
||||
})
|
||||
|
||||
const { reportUpdate } = useDocumentEvents()
|
||||
|
||||
const operation = id ? 'update' : 'create'
|
||||
|
||||
const auth = collectionConfig ? collectionConfig.auth : undefined
|
||||
@@ -87,11 +90,11 @@ export const DefaultEditView: React.FC = () => {
|
||||
|
||||
const onSave = useCallback(
|
||||
async (json) => {
|
||||
// reportUpdate({
|
||||
// id,
|
||||
// entitySlug: collectionConfig.slug,
|
||||
// updatedAt: json?.result?.updatedAt || new Date().toISOString(),
|
||||
// })
|
||||
reportUpdate({
|
||||
id,
|
||||
entitySlug: collectionConfig.slug,
|
||||
updatedAt: json?.result?.updatedAt || new Date().toISOString(),
|
||||
})
|
||||
|
||||
// if (auth && id === user.id) {
|
||||
// await refreshCookieAsync()
|
||||
@@ -108,7 +111,7 @@ export const DefaultEditView: React.FC = () => {
|
||||
id,
|
||||
onSaveFromContext,
|
||||
// refreshCookieAsync,
|
||||
// reportUpdate
|
||||
reportUpdate,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -155,17 +158,6 @@ export const DefaultEditView: React.FC = () => {
|
||||
}`}
|
||||
type="withoutNav"
|
||||
/>
|
||||
{/* <Meta
|
||||
description={`${isEditing ? t('general:editing') : t('general:creating')} - ${getTranslation(
|
||||
collection.labels.singular,
|
||||
i18n,
|
||||
)}`}
|
||||
keywords={`${getTranslation(collection.labels.singular, i18n)}, Payload, CMS`}
|
||||
title={`${isEditing ? t('general:editing') : t('general:creating')} - ${getTranslation(
|
||||
collection.labels.singular,
|
||||
i18n,
|
||||
)}`}
|
||||
/> */}
|
||||
{BeforeDocument}
|
||||
{preventLeaveWithoutSaving && <LeaveWithoutSaving />}
|
||||
<SetStepNav
|
||||
|
||||
@@ -16,9 +16,11 @@ import { useCallback } from 'react'
|
||||
export const EditViewClient: React.FC<EditViewProps> = () => {
|
||||
const { id, collectionSlug, getDocPermissions, getVersions, globalSlug, setDocumentInfo } =
|
||||
useDocumentInfo()
|
||||
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
} = useConfig()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const { getComponentMap } = useComponentMap()
|
||||
@@ -32,8 +34,8 @@ export const EditViewClient: React.FC<EditViewProps> = () => {
|
||||
|
||||
const onSave = useCallback(
|
||||
async (json: { doc }) => {
|
||||
getVersions()
|
||||
getDocPermissions()
|
||||
void getVersions()
|
||||
void getDocPermissions()
|
||||
|
||||
if (!isEditing) {
|
||||
router.push(`${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}`)
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { AdminViewProps } from '../Root'
|
||||
|
||||
export { generateForgotPasswordMetadata } from './meta'
|
||||
|
||||
export const forgotPasswordBaseClass = 'forgot-password'
|
||||
|
||||
export const ForgotPassword: React.FC<AdminViewProps> = ({ initPageResult }) => {
|
||||
const {
|
||||
req: {
|
||||
@@ -46,7 +48,7 @@ export const ForgotPassword: React.FC<AdminViewProps> = ({ initPageResult }) =>
|
||||
/>
|
||||
</p>
|
||||
<br />
|
||||
<Button buttonStyle="secondary" el="link" to={admin}>
|
||||
<Button Link={Link} buttonStyle="secondary" el="link" to={admin}>
|
||||
{i18n.t('general:backToDashboard')}
|
||||
</Button>
|
||||
</Fragment>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
SetViewActions,
|
||||
UnpublishMany,
|
||||
} from '@payloadcms/ui/elements'
|
||||
import Link from 'next/link'
|
||||
import { formatFilesize } from 'payload/utilities'
|
||||
import React, { Fragment, useEffect } from 'react'
|
||||
|
||||
@@ -156,7 +157,7 @@ export const DefaultListView: React.FC = () => {
|
||||
<div className={`${baseClass}__no-results`}>
|
||||
<p>{i18n.t('general:noResults', { label: getTranslation(labels?.plural, i18n) })}</p>
|
||||
{hasCreatePermission && newDocumentURL && (
|
||||
<Button el="link" to={newDocumentURL}>
|
||||
<Button Link={Link} el="link" to={newDocumentURL}>
|
||||
{i18n.t('general:createNewLabel', {
|
||||
label: getTranslation(labels?.singular, i18n),
|
||||
})}
|
||||
|
||||
@@ -9,7 +9,9 @@ import './index.scss'
|
||||
|
||||
export { generateLoginMetadata } from './meta'
|
||||
|
||||
export const Login: React.FC<AdminViewProps> = ({ baseClass, initPageResult, searchParams }) => {
|
||||
export const loginBaseClass = 'login'
|
||||
|
||||
export const Login: React.FC<AdminViewProps> = ({ initPageResult, searchParams }) => {
|
||||
const { req } = initPageResult
|
||||
|
||||
const {
|
||||
@@ -31,7 +33,7 @@ export const Login: React.FC<AdminViewProps> = ({ baseClass, initPageResult, sea
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={`${baseClass}__brand`}>
|
||||
<div className={`${loginBaseClass}__brand`}>
|
||||
<Logo config={config} />
|
||||
</div>
|
||||
{Array.isArray(beforeLogin) && beforeLogin.map((Component, i) => <Component key={i} />)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
import { Button, Gutter, useConfig, useStepNav, useTranslation } from '@payloadcms/ui'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
// import Meta from '../../utilities/Meta'
|
||||
@@ -41,7 +42,7 @@ const NotFound: React.FC<{
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
<h1>{t('general:nothingFound')}</h1>
|
||||
<p>{t('general:sorryNotFound')}</p>
|
||||
<Button className={`${baseClass}__button`} el="link" to={`${admin}`}>
|
||||
<Button Link={Link} className={`${baseClass}__button`} el="link" to={`${admin}`}>
|
||||
{t('general:backToDashboard')}
|
||||
</Button>
|
||||
</Gutter>
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { AdminViewProps } from '../Root'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'reset-password'
|
||||
export const resetPasswordBaseClass = 'reset-password'
|
||||
|
||||
export { generateResetPasswordMetadata } from './meta'
|
||||
|
||||
@@ -48,8 +48,8 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
|
||||
|
||||
if (user) {
|
||||
return (
|
||||
<MinimalTemplate className={baseClass}>
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<MinimalTemplate className={resetPasswordBaseClass}>
|
||||
<div className={`${resetPasswordBaseClass}__wrap`}>
|
||||
<h1>{i18n.t('authentication:alreadyLoggedIn')}</h1>
|
||||
<p>
|
||||
<Translation
|
||||
@@ -61,7 +61,7 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
|
||||
/>
|
||||
</p>
|
||||
<br />
|
||||
<Button buttonStyle="secondary" el="link" to={admin}>
|
||||
<Button Link={Link} buttonStyle="secondary" el="link" to={admin}>
|
||||
{i18n.t('general:backToDashboard')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -70,8 +70,8 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
|
||||
}
|
||||
|
||||
return (
|
||||
<MinimalTemplate className={baseClass}>
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<MinimalTemplate className={resetPasswordBaseClass}>
|
||||
<div className={`${resetPasswordBaseClass}__wrap`}>
|
||||
<h1>{i18n.t('authentication:resetPassword')}</h1>
|
||||
<Form
|
||||
action={`${serverURL}${api}/${userSlug}/reset-password`}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type { InitPageResult, SanitizedConfig } from 'payload/types'
|
||||
|
||||
import { DefaultTemplate, MinimalTemplate } from '@payloadcms/ui'
|
||||
import { redirect } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import { initPage } from '../../utilities/initPage'
|
||||
@@ -8,15 +11,13 @@ import { Account } from '../Account'
|
||||
import { CreateFirstUser } from '../CreateFirstUser'
|
||||
import { Dashboard } from '../Dashboard'
|
||||
import { Document as DocumentView } from '../Document'
|
||||
import { ForgotPassword } from '../ForgotPassword'
|
||||
import { ForgotPassword, forgotPasswordBaseClass } from '../ForgotPassword'
|
||||
import { ListView } from '../List'
|
||||
import { Login } from '../Login'
|
||||
import { Login, loginBaseClass } from '../Login'
|
||||
import { Logout, LogoutInactivity } from '../Logout'
|
||||
import { ResetPassword } from '../ResetPassword'
|
||||
import { ResetPassword, resetPasswordBaseClass } from '../ResetPassword'
|
||||
import { Unauthorized } from '../Unauthorized'
|
||||
import { Verify } from '../Verify'
|
||||
import { Metadata } from 'next'
|
||||
import { I18n } from '@payloadcms/translations'
|
||||
import { Verify, verifyBaseClass } from '../Verify'
|
||||
|
||||
export { generatePageMetadata } from './meta'
|
||||
|
||||
@@ -37,17 +38,16 @@ export type GenerateViewMetadata = (args: {
|
||||
}) => Promise<Metadata>
|
||||
|
||||
export type AdminViewProps = {
|
||||
baseClass?: string
|
||||
initPageResult: InitPageResult
|
||||
params?: { [key: string]: string | string[] | undefined }
|
||||
searchParams: { [key: string]: string | string[] | undefined }
|
||||
}
|
||||
|
||||
const baseClasses = {
|
||||
forgot: 'forgot-password',
|
||||
login: 'login',
|
||||
reset: 'reset-password',
|
||||
verify: 'verify',
|
||||
forgot: forgotPasswordBaseClass,
|
||||
login: loginBaseClass,
|
||||
reset: resetPasswordBaseClass,
|
||||
verify: verifyBaseClass,
|
||||
}
|
||||
|
||||
const oneSegmentViews = {
|
||||
@@ -61,12 +61,18 @@ const oneSegmentViews = {
|
||||
|
||||
export const RootPage = async ({ config: configPromise, params, searchParams }: Args) => {
|
||||
const config = await configPromise
|
||||
|
||||
const {
|
||||
admin: { user: userSlug },
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
let ViewToRender: React.FC<AdminViewProps>
|
||||
let templateClassName
|
||||
let initPageResult: InitPageResult
|
||||
let templateType: 'default' | 'minimal' = 'default'
|
||||
|
||||
let route = config.routes.admin
|
||||
let route = adminRoute
|
||||
|
||||
if (Array.isArray(params.segments)) {
|
||||
route = route + '/' + params.segments.join('/')
|
||||
@@ -195,6 +201,23 @@ export const RootPage = async ({ config: configPromise, params, searchParams }:
|
||||
break
|
||||
}
|
||||
|
||||
const dbHasUser = await initPageResult.req.payload.db
|
||||
.findOne({
|
||||
collection: userSlug,
|
||||
req: initPageResult.req,
|
||||
})
|
||||
?.then((doc) => !!doc)
|
||||
|
||||
const createFirstUserRoute = `${adminRoute}/create-first-user`
|
||||
|
||||
if (!dbHasUser && route !== createFirstUserRoute) {
|
||||
redirect(createFirstUserRoute)
|
||||
}
|
||||
|
||||
if (dbHasUser && route === createFirstUserRoute) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
if (initPageResult) {
|
||||
if (templateType === 'minimal') {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
import { Button, useTranslation } from '@payloadcms/ui'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
export const UnauthorizedClient: React.FC<{ logoutRoute: string }> = ({ logoutRoute }) => {
|
||||
@@ -10,7 +11,7 @@ export const UnauthorizedClient: React.FC<{ logoutRoute: string }> = ({ logoutRo
|
||||
<h2>{t('error:unauthorized')}</h2>
|
||||
<p>{t('error:notAllowedToAccessPage')}</p>
|
||||
<br />
|
||||
<Button el="link" to={logoutRoute}>
|
||||
<Button Link={Link} el="link" to={logoutRoute}>
|
||||
{t('authentication:logOut')}
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { AdminViewProps } from '../Root'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'verify'
|
||||
export const verifyBaseClass = 'verify'
|
||||
|
||||
export { generateVerifyMetadata } from './meta'
|
||||
|
||||
@@ -41,7 +41,7 @@ export const Verify: React.FC<AdminViewProps> = async ({ initPageResult, params
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={`${baseClass}__brand`}>
|
||||
<div className={`${verifyBaseClass}__brand`}>
|
||||
<Logo config={config} />
|
||||
</div>
|
||||
<h2>{textToRender}</h2>
|
||||
|
||||
@@ -14,11 +14,13 @@ const withPayload = (nextConfig = {}) => {
|
||||
},
|
||||
serverComponentsExternalPackages: [
|
||||
...(nextConfig?.experimental?.serverComponentsExternalPackages || []),
|
||||
'@payloadcms/db-mongodb',
|
||||
'drizzle-kit',
|
||||
'drizzle-kit/payload',
|
||||
'libsql',
|
||||
'pino',
|
||||
'pino-pretty',
|
||||
'payload',
|
||||
],
|
||||
},
|
||||
webpack: (webpackConfig, webpackOptions) => {
|
||||
@@ -67,4 +69,4 @@ const withPayload = (nextConfig = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { withPayload }
|
||||
module.exports = withPayload
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
@@ -113,10 +113,7 @@
|
||||
"rimraf": "3.0.2",
|
||||
"serve-static": "1.15.0",
|
||||
"ts-essentials": "7.0.3",
|
||||
"sharp": "0.33.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"sharp": "0.33.2"
|
||||
"sharp": "0.32.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17.0"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Translations } from '@payloadcms/translations'
|
||||
|
||||
import type { DocumentPermissions, Permissions, User } from '../../auth'
|
||||
import type { SanitizedCollectionConfig } from '../../collections/config/types'
|
||||
import type { SanitizedGlobalConfig } from '../../globals/config/types'
|
||||
@@ -30,10 +32,12 @@ export type EditViewProps = {
|
||||
|
||||
export type InitPageResult = {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
cookies: Map<string, string>
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
locale: Locale
|
||||
permissions: Permissions
|
||||
req: PayloadRequest
|
||||
translations: Translations
|
||||
}
|
||||
|
||||
export type ServerSideEditViewProps = EditViewProps & {
|
||||
|
||||
@@ -125,6 +125,7 @@ export const createClientConfig = async (
|
||||
delete clientConfig.db
|
||||
delete clientConfig.editor
|
||||
delete clientConfig.plugins
|
||||
delete clientConfig.sharp
|
||||
|
||||
'localization' in clientConfig &&
|
||||
clientConfig.localization &&
|
||||
|
||||
@@ -177,6 +177,7 @@ export default joi.object({
|
||||
|
||||
return value
|
||||
}),
|
||||
sharp: joi.any(),
|
||||
telemetry: joi.boolean(),
|
||||
typescript: joi.object({
|
||||
declare: joi.boolean(),
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Transporter } from 'nodemailer'
|
||||
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
|
||||
import type { DestinationStream, LoggerOptions } from 'pino'
|
||||
import type React from 'react'
|
||||
import type { default as sharp } from 'sharp'
|
||||
import type { DeepRequired } from 'ts-essentials'
|
||||
|
||||
import type { Payload } from '..'
|
||||
@@ -359,6 +360,23 @@ export type LocalizationConfig = Prettify<
|
||||
LocalizationConfigWithLabels | LocalizationConfigWithNoLabels
|
||||
>
|
||||
|
||||
export type SharpDependency = (
|
||||
input?:
|
||||
| ArrayBuffer
|
||||
| Buffer
|
||||
| Float32Array
|
||||
| Float64Array
|
||||
| Int8Array
|
||||
| Int16Array
|
||||
| Int32Array
|
||||
| Uint8Array
|
||||
| Uint8ClampedArray
|
||||
| Uint16Array
|
||||
| Uint32Array
|
||||
| string,
|
||||
options?: sharp.SharpOptions,
|
||||
) => sharp.Sharp
|
||||
|
||||
/**
|
||||
* This is the central configuration
|
||||
*
|
||||
@@ -635,6 +653,11 @@ export type Config = {
|
||||
* @see https://payloadcms.com/docs/configuration/overview#options
|
||||
*/
|
||||
serverURL?: string
|
||||
/**
|
||||
* Pass in a local copy of Sharp if you'd like to use it.
|
||||
*
|
||||
*/
|
||||
sharp?: SharpDependency
|
||||
/** Send anonymous telemetry data about general usage. */
|
||||
telemetry?: boolean
|
||||
/** Control how typescript interfaces are generated from your collections. */
|
||||
|
||||
@@ -35,7 +35,8 @@ export const readMigrationFiles = async ({
|
||||
|
||||
return Promise.all(
|
||||
files.map(async (filePath) => {
|
||||
const migration = eval(`(await import(${filePath}))`) as Migration
|
||||
// eval used to circumvent errors bundling
|
||||
const migration = eval(`require('${filePath.replaceAll('\\', '/')}')`)
|
||||
migration.name = path.basename(filePath).split('.')?.[0]
|
||||
return migration
|
||||
}),
|
||||
|
||||
@@ -130,9 +130,13 @@ export interface BaseDatabaseAdapter {
|
||||
updateVersion: UpdateVersion
|
||||
}
|
||||
|
||||
export type Init = (payload: Payload) => Promise<void>
|
||||
export type Init = (payload: Payload) => Promise<void> | void
|
||||
|
||||
export type Connect = (payload: Payload) => Promise<void>
|
||||
type ConnectArgs = {
|
||||
hotReload: boolean
|
||||
}
|
||||
|
||||
export type Connect = (args?: ConnectArgs) => Promise<void>
|
||||
|
||||
export type Destroy = (payload: Payload) => Promise<void>
|
||||
|
||||
|
||||
@@ -316,6 +316,7 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
this.globals = {
|
||||
config: this.config.globals,
|
||||
}
|
||||
|
||||
this.config.collections.forEach((collection) => {
|
||||
this.collections[collection.slug] = {
|
||||
config: collection,
|
||||
@@ -330,7 +331,7 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
}
|
||||
|
||||
if (!options.disableDBConnect && this.db.connect) {
|
||||
await this.db.connect(this)
|
||||
await this.db.connect()
|
||||
}
|
||||
|
||||
this.logger.info('Starting Payload...')
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import sharp from 'sharp'
|
||||
|
||||
export const percentToPixel = (value, dimension) => {
|
||||
return Math.floor((parseFloat(value) / 100) * dimension)
|
||||
}
|
||||
|
||||
export default async function cropImage({ cropData, dimensions, file }) {
|
||||
export default async function cropImage({ cropData, dimensions, file, sharp }) {
|
||||
try {
|
||||
const { height, width, x, y } = cropData
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { fromBuffer } from 'file-type'
|
||||
import fs from 'fs'
|
||||
import mkdirp from 'mkdirp'
|
||||
import sanitize from 'sanitize-filename'
|
||||
import sharp from 'sharp'
|
||||
|
||||
import type { Collection } from '../collections/config/types'
|
||||
import type { SanitizedConfig } from '../config/types'
|
||||
@@ -49,6 +48,8 @@ export const generateFileData = async <T>({
|
||||
}
|
||||
}
|
||||
|
||||
const { sharp } = req.payload.config
|
||||
|
||||
let file = req.file
|
||||
|
||||
const { searchParams } = req
|
||||
@@ -113,7 +114,7 @@ export const generateFileData = async <T>({
|
||||
|
||||
if (fileIsAnimated) sharpOptions.animated = true
|
||||
|
||||
if (fileHasAdjustments) {
|
||||
if (fileHasAdjustments && sharp) {
|
||||
if (file.tempFilePath) {
|
||||
sharpFile = sharp(file.tempFilePath, sharpOptions).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
||||
} else {
|
||||
@@ -180,8 +181,8 @@ export const generateFileData = async <T>({
|
||||
fileData.filename = fsSafeName
|
||||
let fileForResize = file
|
||||
|
||||
if (cropData) {
|
||||
const { data: croppedImage, info } = await cropImage({ cropData, dimensions, file })
|
||||
if (cropData && sharp) {
|
||||
const { data: croppedImage, info } = await cropImage({ cropData, dimensions, file, sharp })
|
||||
|
||||
filesToSave.push({
|
||||
buffer: croppedImage,
|
||||
@@ -223,7 +224,7 @@ export const generateFileData = async <T>({
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(imageSizes) && fileSupportsResize) {
|
||||
if (Array.isArray(imageSizes) && fileSupportsResize && sharp) {
|
||||
req.payloadUploadSizes = {}
|
||||
const { sizeData, sizesToSave } = await resizeAndTransformImageSizes({
|
||||
config: collectionConfig,
|
||||
@@ -238,6 +239,7 @@ export const generateFileData = async <T>({
|
||||
mimeType: fileData.mimeType,
|
||||
req,
|
||||
savedFilename: fsSafeName || file.name,
|
||||
sharp,
|
||||
staticPath,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { OutputInfo } from 'sharp'
|
||||
import type { OutputInfo, default as Sharp } from 'sharp'
|
||||
import type sharp from 'sharp'
|
||||
|
||||
import { fromBuffer } from 'file-type'
|
||||
import fs from 'fs'
|
||||
import sanitize from 'sanitize-filename'
|
||||
import sharp from 'sharp'
|
||||
|
||||
import type { SanitizedCollectionConfig } from '../collections/config/types'
|
||||
import type { SharpDependency } from '../exports/config'
|
||||
import type { UploadEdits } from '../exports/types'
|
||||
import type { CustomPayloadRequest, PayloadRequest } from '../types'
|
||||
import type { FileSize, FileSizes, FileToSave, ImageSize, ProbedImageSize } from './types'
|
||||
@@ -24,6 +25,7 @@ type ResizeArgs = {
|
||||
}
|
||||
}
|
||||
savedFilename: string
|
||||
sharp: SharpDependency
|
||||
staticPath: string
|
||||
}
|
||||
|
||||
@@ -213,6 +215,7 @@ export default async function resizeAndTransformImageSizes({
|
||||
mimeType,
|
||||
req,
|
||||
savedFilename,
|
||||
sharp,
|
||||
staticPath,
|
||||
}: ResizeArgs): Promise<ImageSizesResult> {
|
||||
const { imageSizes } = config.upload
|
||||
|
||||
@@ -26,14 +26,19 @@ const getLogger = (
|
||||
name = 'payload',
|
||||
options?: pino.LoggerOptions,
|
||||
destination?: pino.DestinationStream,
|
||||
): PayloadLogger =>
|
||||
pino(
|
||||
{
|
||||
name: options?.name || name,
|
||||
enabled: process.env.DISABLE_LOGGING !== 'true',
|
||||
...(options || defaultLoggerOptions),
|
||||
},
|
||||
destination,
|
||||
)
|
||||
): PayloadLogger => {
|
||||
if (options) {
|
||||
return pino(
|
||||
{
|
||||
name: options?.name || name,
|
||||
enabled: process.env.DISABLE_LOGGING !== 'true',
|
||||
...(options || defaultLoggerOptions),
|
||||
},
|
||||
destination,
|
||||
)
|
||||
}
|
||||
|
||||
return pino(prettySyncLoggerDestination)
|
||||
}
|
||||
|
||||
export default getLogger
|
||||
|
||||
0
packages/payload/src/utilities/pinoPretty.ts
Normal file
0
packages/payload/src/utilities/pinoPretty.ts
Normal file
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"version": "1.1.2",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
@@ -10,7 +10,7 @@
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo} && rimraf dev/yarn.lock",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build && pnpm test",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"test": "jest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "2.2.1",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"homepage:": "https://payloadcms.com",
|
||||
"repository": "git@github.com:payloadcms/plugin-seo.git",
|
||||
"description": "SEO plugin for Payload",
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import type { FieldType, Options } from '@payloadcms/ui'
|
||||
import type { TextareaField } from 'payload/types'
|
||||
import type { FieldType, FormFieldBase, Options } from '@payloadcms/ui'
|
||||
|
||||
import { Textarea, useAllFormFields, useDocumentInfo, useField, useLocale } from '@payloadcms/ui'
|
||||
import { useFieldPath } from '@payloadcms/ui'
|
||||
import { useTranslation } from '@payloadcms/ui'
|
||||
import { TextareaInput } from '@payloadcms/ui'
|
||||
import { useAllFormFields, useDocumentInfo, useField, useLocale } from '@payloadcms/ui'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { PluginConfig } from '../types'
|
||||
import type { GenerateDescription } from '../types'
|
||||
|
||||
import { defaults } from '../defaults'
|
||||
import { LengthIndicator } from '../ui/LengthIndicator'
|
||||
@@ -15,42 +16,47 @@ import { LengthIndicator } from '../ui/LengthIndicator'
|
||||
const { maxLength, minLength } = defaults.description
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
type MetaDescriptionProps = TextareaField & {
|
||||
type MetaDescriptionProps = FormFieldBase & {
|
||||
hasGenerateDescriptionFn: boolean
|
||||
path: string
|
||||
pluginConfig: PluginConfig
|
||||
}
|
||||
|
||||
export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
||||
const { name, label, path, pluginConfig, required } = props
|
||||
const { Label, hasGenerateDescriptionFn, path, required } = props
|
||||
const { path: pathFromContext, schemaPath } = useFieldPath()
|
||||
|
||||
const { t } = useTranslation('plugin-seo')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const locale = useLocale()
|
||||
const [fields] = useAllFormFields()
|
||||
const docInfo = useDocumentInfo()
|
||||
|
||||
const field: FieldType<string> = useField({
|
||||
name,
|
||||
label,
|
||||
path,
|
||||
} as Options)
|
||||
|
||||
const { errorMessage, setValue, showError, value } = field
|
||||
|
||||
const regenerateDescription = useCallback(async () => {
|
||||
const { generateDescription } = pluginConfig
|
||||
let generatedDescription
|
||||
if (!hasGenerateDescriptionFn) return
|
||||
|
||||
if (typeof generateDescription === 'function') {
|
||||
generatedDescription = await generateDescription({
|
||||
const genDescriptionResponse = await fetch('/api/plugin-seo/generate-description', {
|
||||
body: JSON.stringify({
|
||||
...docInfo,
|
||||
doc: { ...fields },
|
||||
locale: typeof locale === 'object' ? locale?.code : locale,
|
||||
})
|
||||
}
|
||||
} satisfies Parameters<GenerateDescription>[0]),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
setValue(generatedDescription)
|
||||
}, [fields, setValue, pluginConfig, locale, docInfo])
|
||||
const { result: generatedDescription } = await genDescriptionResponse.json()
|
||||
|
||||
setValue(generatedDescription || '')
|
||||
}, [fields, setValue, hasGenerateDescriptionFn, locale, docInfo])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -64,8 +70,8 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{label && typeof label === 'string' && label}
|
||||
<div className="plugin-seo__field">
|
||||
{Label}
|
||||
|
||||
{required && (
|
||||
<span
|
||||
@@ -78,7 +84,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{typeof pluginConfig.generateDescription === 'function' && (
|
||||
{hasGenerateDescriptionFn && (
|
||||
<React.Fragment>
|
||||
—
|
||||
<button
|
||||
@@ -94,7 +100,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{t('autoGenerate')}
|
||||
{t('plugin-seo:autoGenerate')}
|
||||
</button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
@@ -104,13 +110,13 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
||||
color: '#9A9A9A',
|
||||
}}
|
||||
>
|
||||
{t('lengthTipDescription', { maxLength, minLength })}
|
||||
{t('plugin-seo:lengthTipDescription', { maxLength, minLength })}
|
||||
<a
|
||||
href="https://developers.google.com/search/docs/advanced/appearance/snippet#meta-descriptions"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('bestPractices')}
|
||||
{t('plugin-seo:bestPractices')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,11 +126,10 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Textarea
|
||||
errorMessage={errorMessage}
|
||||
name={name}
|
||||
<TextareaInput
|
||||
Error={errorMessage} // TODO: Fix
|
||||
onChange={setValue}
|
||||
path={name}
|
||||
path={pathFromContext}
|
||||
required={required}
|
||||
showError={showError}
|
||||
style={{
|
||||
@@ -145,7 +150,3 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getMetaDescriptionField = (props: MetaDescriptionProps) => (
|
||||
<MetaDescription {...props} />
|
||||
)
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import type { Props as UploadInputProps } from 'payload/components/fields/Upload'
|
||||
import type { FieldType, Options } from 'payload/dist/admin/components/forms/useField/types'
|
||||
import type { FieldType, Options, UploadInputProps } from '@payloadcms/ui'
|
||||
|
||||
import { UploadInput, useAllFormFields, useField } from 'payload/components/forms'
|
||||
import { useConfig, useDocumentInfo, useLocale } from 'payload/components/utilities'
|
||||
import {
|
||||
UploadInput,
|
||||
useAllFormFields,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useField,
|
||||
useLocale,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { PluginConfig } from '../types'
|
||||
import type { GenerateImage } from '../types'
|
||||
|
||||
import { Pill } from '../ui/Pill'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
type MetaImageProps = UploadInputProps & {
|
||||
path: string
|
||||
pluginConfig: PluginConfig
|
||||
hasGenerateImageFn: boolean
|
||||
}
|
||||
|
||||
export const MetaImage: React.FC<MetaImageProps> = (props) => {
|
||||
const { name, fieldTypes, label, pluginConfig, relationTo, required } = props || {}
|
||||
const { Label, hasGenerateImageFn, path, relationTo, required } = props || {}
|
||||
|
||||
const field: FieldType<string> = useField(props as Options)
|
||||
|
||||
const { t } = useTranslation('plugin-seo')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const locale = useLocale()
|
||||
const [fields] = useAllFormFields()
|
||||
@@ -32,19 +36,25 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
|
||||
const { errorMessage, setValue, showError, value } = field
|
||||
|
||||
const regenerateImage = useCallback(async () => {
|
||||
const { generateImage } = pluginConfig
|
||||
let generatedImage
|
||||
if (!hasGenerateImageFn) return
|
||||
|
||||
if (typeof generateImage === 'function') {
|
||||
generatedImage = await generateImage({
|
||||
const genImageResponse = await fetch('/api/plugin-seo/generate-image', {
|
||||
body: JSON.stringify({
|
||||
...docInfo,
|
||||
doc: { ...fields },
|
||||
locale: typeof locale === 'object' ? locale?.code : locale,
|
||||
})
|
||||
}
|
||||
} satisfies Parameters<GenerateImage>[0]),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
setValue(generatedImage)
|
||||
}, [fields, setValue, pluginConfig, locale, docInfo])
|
||||
const { result: generatedImage } = await genImageResponse.json()
|
||||
|
||||
setValue(generatedImage || '')
|
||||
}, [fields, setValue, hasGenerateImageFn, locale, docInfo])
|
||||
|
||||
const hasImage = Boolean(value)
|
||||
|
||||
@@ -66,8 +76,8 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{label && typeof label === 'string' && label}
|
||||
<div className="plugin-seo__field">
|
||||
{Label}
|
||||
|
||||
{required && (
|
||||
<span
|
||||
@@ -80,7 +90,7 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{typeof pluginConfig.generateImage === 'function' && (
|
||||
{hasGenerateImageFn && (
|
||||
<React.Fragment>
|
||||
—
|
||||
<button
|
||||
@@ -96,18 +106,18 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{t('autoGenerate')}
|
||||
{t('plugin-seo:autoGenerate')}
|
||||
</button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
{typeof pluginConfig.generateImage === 'function' && (
|
||||
{hasGenerateImageFn && (
|
||||
<div
|
||||
style={{
|
||||
color: '#9A9A9A',
|
||||
}}
|
||||
>
|
||||
{t('imageAutoGenerationTip')}
|
||||
{t('plugin-seo:imageAutoGenerationTip')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -118,13 +128,11 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
|
||||
}}
|
||||
>
|
||||
<UploadInput
|
||||
Error={errorMessage} // TODO: Fix
|
||||
api={api}
|
||||
collection={collection}
|
||||
errorMessage={errorMessage}
|
||||
fieldTypes={fieldTypes}
|
||||
filterOptions={{}}
|
||||
label={undefined}
|
||||
name={name}
|
||||
onChange={(incomingImage) => {
|
||||
if (incomingImage !== null) {
|
||||
const { id: incomingID } = incomingImage
|
||||
@@ -133,7 +141,6 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
|
||||
setValue(null)
|
||||
}
|
||||
}}
|
||||
path={name}
|
||||
relationTo={relationTo}
|
||||
required={required}
|
||||
serverURL={serverURL}
|
||||
@@ -154,11 +161,9 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
|
||||
<Pill
|
||||
backgroundColor={hasImage ? 'green' : 'red'}
|
||||
color="white"
|
||||
label={hasImage ? t('good') : t('noImage')}
|
||||
label={hasImage ? t('plugin-seo:good') : t('plugin-seo:noImage')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getMetaImageField = (props: MetaImageProps) => <MetaImage {...props} />
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
'use client'
|
||||
|
||||
import type {
|
||||
FieldType as FieldType,
|
||||
Options,
|
||||
} from 'payload/dist/admin/components/forms/useField/types'
|
||||
import type { TextField as TextFieldType } from 'payload/types'
|
||||
import type { FieldType, FormFieldBase, Options } from '@payloadcms/ui'
|
||||
|
||||
import { TextInput, useAllFormFields, useField } from 'payload/components/forms'
|
||||
import { useDocumentInfo, useLocale } from 'payload/components/utilities'
|
||||
import { useFieldPath } from '@payloadcms/ui'
|
||||
import {
|
||||
TextInput,
|
||||
useAllFormFields,
|
||||
useDocumentInfo,
|
||||
useField,
|
||||
useLocale,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { PluginConfig } from '../types'
|
||||
import type { GenerateTitle } from '../types'
|
||||
|
||||
import { defaults } from '../defaults'
|
||||
import { LengthIndicator } from '../ui/LengthIndicator'
|
||||
import './index.scss'
|
||||
|
||||
const { maxLength, minLength } = defaults.title
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
type MetaTitleProps = TextFieldType & {
|
||||
path: string
|
||||
pluginConfig: PluginConfig
|
||||
type MetaTitleProps = FormFieldBase & {
|
||||
hasGenerateTitleFn: boolean
|
||||
}
|
||||
|
||||
export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
|
||||
const { name, label, path, pluginConfig, required } = props || {}
|
||||
const { Label, hasGenerateTitleFn, path, required } = props || {}
|
||||
const { path: pathFromContext, schemaPath } = useFieldPath()
|
||||
|
||||
const { t } = useTranslation('plugin-seo')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const field: FieldType<string> = useField({
|
||||
name,
|
||||
label,
|
||||
path,
|
||||
} as Options)
|
||||
|
||||
@@ -42,19 +43,25 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
|
||||
const { errorMessage, setValue, showError, value } = field
|
||||
|
||||
const regenerateTitle = useCallback(async () => {
|
||||
const { generateTitle } = pluginConfig
|
||||
let generatedTitle
|
||||
if (!hasGenerateTitleFn) return
|
||||
|
||||
if (typeof generateTitle === 'function') {
|
||||
generatedTitle = await generateTitle({
|
||||
const genTitleResponse = await fetch('/api/plugin-seo/generate-title', {
|
||||
body: JSON.stringify({
|
||||
...docInfo,
|
||||
doc: { ...fields },
|
||||
locale: typeof locale === 'object' ? locale?.code : locale,
|
||||
})
|
||||
}
|
||||
} satisfies Parameters<GenerateTitle>[0]),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
setValue(generatedTitle)
|
||||
}, [fields, setValue, pluginConfig, locale, docInfo])
|
||||
const { result: generatedTitle } = await genTitleResponse.json()
|
||||
|
||||
setValue(generatedTitle || '')
|
||||
}, [fields, setValue, hasGenerateTitleFn, locale, docInfo])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -68,8 +75,8 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{label && typeof label === 'string' && label}
|
||||
<div className="plugin-seo__field">
|
||||
{Label}
|
||||
|
||||
{required && (
|
||||
<span
|
||||
@@ -82,7 +89,7 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{typeof pluginConfig.generateTitle === 'function' && (
|
||||
{hasGenerateTitleFn && (
|
||||
<React.Fragment>
|
||||
—
|
||||
<button
|
||||
@@ -98,7 +105,7 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{t('autoGenerate')}
|
||||
{t('plugin-seo:autoGenerate')}
|
||||
</button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
@@ -108,13 +115,13 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
|
||||
color: '#9A9A9A',
|
||||
}}
|
||||
>
|
||||
{t('lengthTipTitle', { maxLength, minLength })}
|
||||
{t('plugin-seo:lengthTipTitle', { maxLength, minLength })}
|
||||
<a
|
||||
href="https://developers.google.com/search/docs/advanced/appearance/title-link#page-titles"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('bestPractices')}
|
||||
{t('plugin-seo:bestPractices')}
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
@@ -126,10 +133,9 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
errorMessage={errorMessage}
|
||||
name={name}
|
||||
Error={errorMessage} // TODO: fix errormessage
|
||||
onChange={setValue}
|
||||
path={name}
|
||||
path={pathFromContext}
|
||||
required={required}
|
||||
showError={showError}
|
||||
style={{
|
||||
@@ -150,5 +156,3 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getMetaTitleField = (props: MetaTitleProps) => <MetaTitle {...props} />
|
||||
|
||||
5
packages/plugin-seo/src/fields/index.scss
Normal file
5
packages/plugin-seo/src/fields/index.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
.plugin-seo__field {
|
||||
.field-label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,23 @@
|
||||
import type { Config } from 'payload/config'
|
||||
import type { Field, GroupField, TabsField } from 'payload/types'
|
||||
import type { Field, GroupField, TabsField, TextField } from 'payload/types'
|
||||
|
||||
import { deepMerge } from 'payload/utilities'
|
||||
import React from 'react'
|
||||
|
||||
import type { PluginConfig } from './types'
|
||||
import type {
|
||||
GenerateDescription,
|
||||
GenerateImage,
|
||||
GenerateTitle,
|
||||
GenerateURL,
|
||||
PluginConfig,
|
||||
} from './types'
|
||||
|
||||
import { getMetaDescriptionField } from './fields/MetaDescription'
|
||||
import { getMetaImageField } from './fields/MetaImage'
|
||||
import { getMetaTitleField } from './fields/MetaTitle'
|
||||
import { MetaDescription } from './fields/MetaDescription'
|
||||
import { MetaImage } from './fields/MetaImage'
|
||||
import { MetaTitle } from './fields/MetaTitle'
|
||||
import translations from './translations'
|
||||
import { Overview } from './ui/Overview'
|
||||
import { getPreviewField } from './ui/Preview'
|
||||
import { Preview } from './ui/Preview'
|
||||
|
||||
const seo =
|
||||
(pluginConfig: PluginConfig) =>
|
||||
@@ -35,18 +42,30 @@ const seo =
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Field: (props) => getMetaTitleField({ ...props, pluginConfig }),
|
||||
Field: (props) => (
|
||||
<MetaTitle
|
||||
{...props}
|
||||
hasGenerateTitleFn={typeof pluginConfig.generateTitle === 'function'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
localized: true,
|
||||
...(pluginConfig?.fieldOverrides?.title ?? {}),
|
||||
...((pluginConfig?.fieldOverrides?.title as unknown as TextField) ?? {}),
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
admin: {
|
||||
components: {
|
||||
Field: (props) => getMetaDescriptionField({ ...props, pluginConfig }),
|
||||
Field: (props) => (
|
||||
<MetaDescription
|
||||
{...props}
|
||||
hasGenerateDescriptionFn={
|
||||
typeof pluginConfig.generateDescription === 'function'
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
localized: true,
|
||||
@@ -60,7 +79,12 @@ const seo =
|
||||
type: 'upload',
|
||||
admin: {
|
||||
components: {
|
||||
Field: (props) => getMetaImageField({ ...props, pluginConfig }),
|
||||
Field: (props) => (
|
||||
<MetaImage
|
||||
{...props}
|
||||
hasGenerateImageFn={typeof pluginConfig.generateImage === 'function'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
description:
|
||||
'Maximum upload file size: 12MB. Recommended file size for images is <500KB.',
|
||||
@@ -78,7 +102,12 @@ const seo =
|
||||
type: 'ui',
|
||||
admin: {
|
||||
components: {
|
||||
Field: (props) => getPreviewField({ ...props, pluginConfig }),
|
||||
Field: (props) => (
|
||||
<Preview
|
||||
{...props}
|
||||
hasGenerateURLFn={typeof pluginConfig.generateURL === 'function'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
label: 'Preview',
|
||||
@@ -150,6 +179,48 @@ const seo =
|
||||
|
||||
return collection
|
||||
}) || [],
|
||||
endpoints: [
|
||||
{
|
||||
handler: async (req) => {
|
||||
const args: Parameters<GenerateTitle>[0] =
|
||||
req.data as unknown as Parameters<GenerateTitle>[0]
|
||||
const result = await pluginConfig.generateTitle(args)
|
||||
return new Response(JSON.stringify({ result }), { status: 200 })
|
||||
},
|
||||
method: 'post',
|
||||
path: '/plugin-seo/generate-title',
|
||||
},
|
||||
{
|
||||
handler: async (req) => {
|
||||
const args: Parameters<GenerateDescription>[0] =
|
||||
req.data as unknown as Parameters<GenerateDescription>[0]
|
||||
const result = await pluginConfig.generateDescription(args)
|
||||
return new Response(JSON.stringify({ result }), { status: 200 })
|
||||
},
|
||||
method: 'post',
|
||||
path: '/plugin-seo/generate-description',
|
||||
},
|
||||
{
|
||||
handler: async (req) => {
|
||||
const args: Parameters<GenerateURL>[0] =
|
||||
req.data as unknown as Parameters<GenerateURL>[0]
|
||||
const result = await pluginConfig.generateURL(args)
|
||||
return new Response(JSON.stringify({ result }), { status: 200 })
|
||||
},
|
||||
method: 'post',
|
||||
path: '/plugin-seo/generate-url',
|
||||
},
|
||||
{
|
||||
handler: async (req) => {
|
||||
const args: Parameters<GenerateImage>[0] =
|
||||
req.data as unknown as Parameters<GenerateImage>[0]
|
||||
const result = await pluginConfig.generateImage(args)
|
||||
return new Response(result, { status: 200 })
|
||||
},
|
||||
method: 'post',
|
||||
path: '/plugin-seo/generate-image',
|
||||
},
|
||||
],
|
||||
globals:
|
||||
config.globals?.map((global) => {
|
||||
const { slug } = global
|
||||
@@ -198,8 +269,8 @@ const seo =
|
||||
}) || [],
|
||||
i18n: {
|
||||
...config.i18n,
|
||||
resources: {
|
||||
...deepMerge(translations, config.i18n?.resources),
|
||||
translations: {
|
||||
...deepMerge(translations, config.i18n?.translations),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslation } from '@payloadcms/ui'
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Pill } from './Pill'
|
||||
|
||||
@@ -19,7 +19,7 @@ export const LengthIndicator: React.FC<{
|
||||
|
||||
const [label, setLabel] = useState('')
|
||||
const [barWidth, setBarWidth] = useState<number>(0)
|
||||
const { t } = useTranslation('plugin-seo')
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const textLength = text?.length || 0
|
||||
@@ -38,13 +38,13 @@ export const LengthIndicator: React.FC<{
|
||||
const ratioUntilMin = textLength / minLength
|
||||
|
||||
if (ratioUntilMin > 0.9) {
|
||||
setLabel(t('almostThere'))
|
||||
setLabel(t('plugin-seo:almostThere'))
|
||||
setLabelStyle({
|
||||
backgroundColor: 'orange',
|
||||
color: 'white',
|
||||
})
|
||||
} else {
|
||||
setLabel(t('tooShort'))
|
||||
setLabel(t('plugin-seo:tooShort'))
|
||||
setLabelStyle({
|
||||
backgroundColor: 'orangered',
|
||||
color: 'white',
|
||||
@@ -55,7 +55,7 @@ export const LengthIndicator: React.FC<{
|
||||
}
|
||||
|
||||
if (progress >= 0 && progress <= 1) {
|
||||
setLabel(t('good'))
|
||||
setLabel(t('plugin-seo:good'))
|
||||
setLabelStyle({
|
||||
backgroundColor: 'green',
|
||||
color: 'white',
|
||||
@@ -64,7 +64,7 @@ export const LengthIndicator: React.FC<{
|
||||
}
|
||||
|
||||
if (progress > 1) {
|
||||
setLabel(t('tooLong'))
|
||||
setLabel(t('plugin-seo:tooLong'))
|
||||
setLabelStyle({
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
@@ -97,15 +97,17 @@ export const LengthIndicator: React.FC<{
|
||||
}}
|
||||
>
|
||||
<small>
|
||||
{t('characterCount', { current: text?.length || 0, maxLength, minLength })}
|
||||
{t('plugin-seo:characterCount', { current: text?.length || 0, maxLength, minLength })}
|
||||
{(textLength === 0 || charsUntilMin > 0) && (
|
||||
<Fragment>{t('charactersToGo', { characters: charsUntilMin })}</Fragment>
|
||||
<Fragment>{t('plugin-seo:charactersToGo', { characters: charsUntilMin })}</Fragment>
|
||||
)}
|
||||
{charsUntilMin <= 0 && charsUntilMax >= 0 && (
|
||||
<Fragment>{t('charactersLeftOver', { characters: charsUntilMax })}</Fragment>
|
||||
<Fragment>{t('plugin-seo:charactersLeftOver', { characters: charsUntilMax })}</Fragment>
|
||||
)}
|
||||
{charsUntilMax < 0 && (
|
||||
<Fragment>{t('charactersTooMany', { characters: charsUntilMax * -1 })}</Fragment>
|
||||
<Fragment>
|
||||
{t('plugin-seo:charactersTooMany', { characters: charsUntilMax * -1 })}
|
||||
</Fragment>
|
||||
)}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
import type { FormField } from 'payload/types'
|
||||
|
||||
import { useAllFormFields, useForm } from 'payload/components/forms'
|
||||
import { useAllFormFields, useForm, useTranslation } from '@payloadcms/ui'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { defaults } from '../defaults'
|
||||
|
||||
@@ -26,7 +25,7 @@ export const Overview: React.FC = () => {
|
||||
'meta.title': { value: metaTitle } = {} as FormField,
|
||||
},
|
||||
] = useAllFormFields()
|
||||
const { t } = useTranslation('plugin-seo')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [titleIsValid, setTitleIsValid] = useState<boolean | undefined>()
|
||||
const [descIsValid, setDescIsValid] = useState<boolean | undefined>()
|
||||
@@ -60,7 +59,9 @@ export const Overview: React.FC = () => {
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
<div>{t('checksPassing', { current: numberOfPasses, max: testResults.length })}</div>
|
||||
<div>
|
||||
{t('plugin-seo:checksPassing', { current: numberOfPasses, max: testResults.length })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,24 +2,18 @@
|
||||
|
||||
import type { FormField, UIField } from 'payload/types'
|
||||
|
||||
import { useAllFormFields } from 'payload/components/forms'
|
||||
import { useDocumentInfo, useLocale } from 'payload/components/utilities'
|
||||
import { useAllFormFields, useDocumentInfo, useLocale, useTranslation } from '@payloadcms/ui'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { PluginConfig } from '../types'
|
||||
import type { GenerateURL } from '../types'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
type PreviewProps = UIField & {
|
||||
pluginConfig: PluginConfig
|
||||
hasGenerateURLFn: boolean
|
||||
}
|
||||
|
||||
export const Preview: React.FC<PreviewProps> = (props) => {
|
||||
const {
|
||||
pluginConfig: { generateURL },
|
||||
} = props || {}
|
||||
|
||||
const { t } = useTranslation('plugin-seo')
|
||||
export const Preview: React.FC<PreviewProps> = ({ hasGenerateURLFn }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const locale = useLocale()
|
||||
const [fields] = useAllFormFields()
|
||||
@@ -34,30 +28,39 @@ export const Preview: React.FC<PreviewProps> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
const getHref = async () => {
|
||||
if (typeof generateURL === 'function' && !href) {
|
||||
const newHref = await generateURL({
|
||||
const genURLResponse = await fetch('/api/plugin-seo/generate-url', {
|
||||
body: JSON.stringify({
|
||||
...docInfo,
|
||||
doc: { ...fields },
|
||||
locale: typeof locale === 'object' ? locale?.code : locale,
|
||||
})
|
||||
} satisfies Parameters<GenerateURL>[0]),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
setHref(newHref)
|
||||
}
|
||||
const { result: newHref } = await genURLResponse.json()
|
||||
|
||||
setHref(newHref)
|
||||
}
|
||||
|
||||
getHref() // eslint-disable-line @typescript-eslint/no-floating-promises
|
||||
}, [generateURL, fields, href, locale, docInfo])
|
||||
if (hasGenerateURLFn && !href) {
|
||||
void getHref()
|
||||
}
|
||||
}, [fields, href, locale, docInfo, hasGenerateURLFn])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{t('preview')}</div>
|
||||
<div>{t('plugin-seo:preview')}</div>
|
||||
<div
|
||||
style={{
|
||||
color: '#9A9A9A',
|
||||
marginBottom: '5px',
|
||||
}}
|
||||
>
|
||||
{t('previewDescription')}
|
||||
{t('plugin-seo:previewDescription')}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@@ -105,5 +108,3 @@ export const Preview: React.FC<PreviewProps> = (props) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getPreviewField = (props: PreviewProps) => <Preview {...props} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "0.7.0",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -82,7 +82,6 @@ export const getGenerateComponentMap =
|
||||
const mappedFields = mapFields({
|
||||
config,
|
||||
fieldSchema: sanitizedFields,
|
||||
operation: 'update',
|
||||
permissions: {},
|
||||
readOnly: false,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -70,7 +70,7 @@ export const getGenerateComponentMap =
|
||||
switch (element.name) {
|
||||
case 'link': {
|
||||
const linkFields = sanitizeFields({
|
||||
config: config,
|
||||
config,
|
||||
fields: transformExtraFields(args.admin?.link?.fields, config, i18n),
|
||||
validRelationships,
|
||||
})
|
||||
@@ -78,7 +78,6 @@ export const getGenerateComponentMap =
|
||||
const mappedFields = mapFields({
|
||||
config,
|
||||
fieldSchema: linkFields,
|
||||
operation: 'update',
|
||||
permissions: {},
|
||||
readOnly: false,
|
||||
})
|
||||
@@ -110,7 +109,6 @@ export const getGenerateComponentMap =
|
||||
const mappedFields = mapFields({
|
||||
config,
|
||||
fieldSchema: uploadFields,
|
||||
operation: 'update',
|
||||
permissions: {},
|
||||
readOnly: false,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/translations",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"main": "./dist/exports/index.ts",
|
||||
"types": "./dist/types.d.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
export type LanguageTranslations = {
|
||||
[namespace: string]: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export type Translations = {
|
||||
[language: string]:
|
||||
| {
|
||||
$schema: string
|
||||
}
|
||||
| {
|
||||
[namespace: string]: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
| LanguageTranslations
|
||||
}
|
||||
|
||||
export type TFunction = (key: string, options?: Record<string, any>) => string
|
||||
@@ -19,6 +21,7 @@ export type I18n = {
|
||||
language: string
|
||||
/** Translate function */
|
||||
t: (key: string, options?: Record<string, unknown>) => string
|
||||
translations: Translations
|
||||
}
|
||||
|
||||
export type I18nOptions = {
|
||||
@@ -29,11 +32,7 @@ export type I18nOptions = {
|
||||
| {
|
||||
$schema: string
|
||||
}
|
||||
| {
|
||||
[namespace: string]: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
| LanguageTranslations
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +40,10 @@ export type InitTFunction = (args: {
|
||||
config: I18nOptions
|
||||
language?: string
|
||||
translations?: Translations
|
||||
}) => TFunction
|
||||
}) => {
|
||||
t: TFunction
|
||||
translations: Translations
|
||||
}
|
||||
|
||||
export type InitI18n = (args: {
|
||||
config: I18nOptions
|
||||
|
||||
@@ -79,7 +79,8 @@ const replaceVars = ({
|
||||
.map((part) => {
|
||||
if (part.startsWith('{{') && part.endsWith('}}')) {
|
||||
const placeholder = part.substring(2, part.length - 2).trim()
|
||||
return vars[placeholder] || part
|
||||
const value = vars[placeholder]
|
||||
return value !== undefined && value !== null ? value : part
|
||||
} else {
|
||||
return part
|
||||
}
|
||||
@@ -191,23 +192,27 @@ export function matchLanguage(header: string): string | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const initTFunction: InitTFunction = (args) => (key, vars) => {
|
||||
const initTFunction: InitTFunction = (args) => {
|
||||
const { config, language, translations } = args
|
||||
|
||||
const mergedLanguages = deepMerge(config?.translations ?? {}, translations)
|
||||
const mergedTranslations = deepMerge(config?.translations ?? {}, translations)
|
||||
const languagePreference = matchLanguage(language)
|
||||
|
||||
return t({
|
||||
key,
|
||||
translations: mergedLanguages[languagePreference],
|
||||
vars,
|
||||
})
|
||||
return {
|
||||
translations: mergedTranslations,
|
||||
t: (key, vars) => {
|
||||
return t({
|
||||
key,
|
||||
translations: mergedTranslations[languagePreference],
|
||||
vars,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function memoize<T>(fn: Function, keys: string[]): T {
|
||||
function memoize(fn: Function, keys: string[]) {
|
||||
const cacheMap = new Map()
|
||||
|
||||
return <T>function (args) {
|
||||
const memoized = (args) => {
|
||||
const cacheKey = keys.reduce((acc, key) => acc + args[key], '')
|
||||
|
||||
if (!cacheMap.has(cacheKey)) {
|
||||
@@ -217,21 +222,31 @@ function memoize<T>(fn: Function, keys: string[]): T {
|
||||
|
||||
return cacheMap.get(cacheKey)!
|
||||
}
|
||||
|
||||
return memoized
|
||||
}
|
||||
|
||||
export const initI18n: InitI18n = memoize(
|
||||
<InitI18n>(({ config, language = 'en', translations, context }) => {
|
||||
const i18n = {
|
||||
({
|
||||
config,
|
||||
language = 'en',
|
||||
translations: incomingTranslations,
|
||||
context,
|
||||
}: Parameters<InitI18n>[0]) => {
|
||||
const { t, translations } = initTFunction({
|
||||
config,
|
||||
language: language || config.fallbackLanguage,
|
||||
translations: incomingTranslations,
|
||||
})
|
||||
|
||||
const i18n: I18n = {
|
||||
fallbackLanguage: config.fallbackLanguage,
|
||||
language: language || config.fallbackLanguage,
|
||||
t: initTFunction({
|
||||
config,
|
||||
language: language || config.fallbackLanguage,
|
||||
translations,
|
||||
}),
|
||||
t,
|
||||
translations,
|
||||
}
|
||||
|
||||
return i18n
|
||||
}),
|
||||
},
|
||||
['language', 'context'] satisfies Array<keyof Parameters<InitI18n>[0]>,
|
||||
)
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/ui",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"version": "3.0.0-alpha.18",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -49,6 +49,7 @@ export const ButtonContents = ({ children, icon, showTooltip, tooltip }) => {
|
||||
export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, Props>((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
type = 'button',
|
||||
Link,
|
||||
'aria-label': ariaLabel,
|
||||
buttonStyle = 'primary',
|
||||
@@ -65,7 +66,6 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, Props>((
|
||||
size = 'medium',
|
||||
to,
|
||||
tooltip,
|
||||
type = 'button',
|
||||
url,
|
||||
} = props
|
||||
|
||||
@@ -95,6 +95,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, Props>((
|
||||
|
||||
const buttonProps = {
|
||||
id,
|
||||
type,
|
||||
'aria-disabled': disabled,
|
||||
'aria-label': ariaLabel,
|
||||
className: classes,
|
||||
@@ -104,13 +105,12 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, Props>((
|
||||
onMouseLeave: tooltip ? () => setShowTooltip(false) : undefined,
|
||||
rel: newTab ? 'noopener noreferrer' : undefined,
|
||||
target: newTab ? '_blank' : undefined,
|
||||
type,
|
||||
}
|
||||
|
||||
switch (el) {
|
||||
case 'link':
|
||||
if (!Link) {
|
||||
console.error('Link is required when using el="link"')
|
||||
console.error('Link is required when using el="link"', children)
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { Description } from 'payload/types'
|
||||
import type { Description, Operation } from 'payload/types'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import qs from 'qs'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import { useConfig } from '../../providers/Config'
|
||||
@@ -22,10 +22,8 @@ const Localizer: React.FC<{
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const { searchParams } = useSearchParams()
|
||||
|
||||
const localeLabel = getTranslation(locale.label, i18n)
|
||||
|
||||
const { dispatchSearchParams, searchParams } = useSearchParams()
|
||||
const router = useRouter()
|
||||
if (localization) {
|
||||
const { locales } = localization
|
||||
|
||||
@@ -36,42 +34,34 @@ const Localizer: React.FC<{
|
||||
horizontalAlign="right"
|
||||
render={({ close }) => (
|
||||
<PopupList.ButtonGroup>
|
||||
<React.Fragment>
|
||||
{locale ? (
|
||||
{locales.map((localeOption) => {
|
||||
const newParams = {
|
||||
...searchParams,
|
||||
locale: localeOption.code,
|
||||
}
|
||||
const localeOptionLabel = getTranslation(localeOption.label, i18n)
|
||||
|
||||
return (
|
||||
<PopupList.Button
|
||||
active
|
||||
href={{
|
||||
search: qs.stringify({
|
||||
...searchParams,
|
||||
locale: locale.code,
|
||||
}),
|
||||
active={locale.code === localeOption.code}
|
||||
href={{ query: newParams }}
|
||||
key={localeOption.code}
|
||||
onClick={() => {
|
||||
close()
|
||||
dispatchSearchParams({
|
||||
type: 'set',
|
||||
params: {
|
||||
locale: searchParams.locale,
|
||||
},
|
||||
})
|
||||
router.refresh()
|
||||
}}
|
||||
key={locale.code}
|
||||
onClick={close}
|
||||
>
|
||||
{localeLabel}
|
||||
{localeLabel !== locale.code && ` (${locale.code})`}
|
||||
{localeOptionLabel}
|
||||
{localeOptionLabel !== localeOption.code && ` (${localeOption.code})`}
|
||||
</PopupList.Button>
|
||||
) : null}
|
||||
|
||||
{locales.map((localeOption) => {
|
||||
if (locale.code === localeOption.code) return null
|
||||
|
||||
const newParams = {
|
||||
...searchParams,
|
||||
locale: localeOption.code,
|
||||
}
|
||||
const search = qs.stringify(newParams)
|
||||
const localeOptionLabel = getTranslation(localeOption.label, i18n)
|
||||
|
||||
return (
|
||||
<PopupList.Button href={{ search }} key={localeOption.code} onClick={close}>
|
||||
{localeOptionLabel}
|
||||
{localeOptionLabel !== localeOption.code && ` (${localeOption.code})`}
|
||||
</PopupList.Button>
|
||||
)
|
||||
})}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</PopupList.ButtonGroup>
|
||||
)}
|
||||
showScrollbar
|
||||
|
||||
@@ -33,8 +33,14 @@ export type { OnChange } from '../forms/fields/RadioGroup/types'
|
||||
export { default as Select } from '../forms/fields/Select'
|
||||
export { default as SelectInput } from '../forms/fields/Select'
|
||||
export { default as Text } from '../forms/fields/Text'
|
||||
export { TextInput, type TextInputProps } from '../forms/fields/Text/Input'
|
||||
export type { Props as TextFieldProps } from '../forms/fields/Text/types'
|
||||
export { default as Textarea } from '../forms/fields/Textarea'
|
||||
export { type TextAreaInputProps, TextareaInput } from '../forms/fields/Textarea/Input'
|
||||
export { default as UploadField } from '../forms/fields/Upload'
|
||||
|
||||
export { UploadInput, type UploadInputProps } from '../forms/fields/Upload/Input'
|
||||
|
||||
export { fieldBaseClass } from '../forms/fields/shared'
|
||||
export { default as useField } from '../forms/useField'
|
||||
export type { FieldType, Options } from '../forms/useField/types'
|
||||
|
||||
@@ -19,6 +19,7 @@ export { EditDepthContext, EditDepthProvider } from '../providers/EditDepth'
|
||||
export { useEditDepth } from '../providers/EditDepth'
|
||||
export { FormQueryParams, FormQueryParamsProvider } from '../providers/FormQueryParams'
|
||||
export type { QueryParamTypes } from '../providers/FormQueryParams'
|
||||
export { useFormQueryParams } from '../providers/FormQueryParams'
|
||||
export { useListInfo } from '../providers/ListInfo'
|
||||
export { ListInfoProvider } from '../providers/ListInfo'
|
||||
export type { ColumnPreferences } from '../providers/ListInfo/types'
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
.field-description {
|
||||
display: flex;
|
||||
color: var(--theme-elevation-400);
|
||||
margin-top: calc(var(--base) / 4);
|
||||
|
||||
&--margin-bottom {
|
||||
margin-top: 0;
|
||||
margin-bottom: calc(var(--base) / 2);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ type FieldPathContextType = {
|
||||
path: string
|
||||
schemaPath: string
|
||||
}
|
||||
|
||||
const FieldPathContext = React.createContext<FieldPathContextType>({
|
||||
path: '',
|
||||
schemaPath: '',
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export type Props = {
|
||||
className?: string
|
||||
description?: string
|
||||
marginPlacement?: 'bottom' | 'top'
|
||||
value?: unknown
|
||||
}
|
||||
@@ -556,7 +556,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
executeOnChange() // eslint-disable-line @typescript-eslint/no-floating-promises
|
||||
void executeOnChange() // eslint-disable-line @typescript-eslint/no-floating-promises
|
||||
},
|
||||
150,
|
||||
[fields, dispatchFields, onChange],
|
||||
|
||||
@@ -11,7 +11,7 @@ export const mergeServerFormState = (
|
||||
let changed = false
|
||||
|
||||
Object.entries(newState).forEach(([path, newFieldState]) => {
|
||||
newFieldState.initialValue = oldState[path].initialValue
|
||||
newFieldState.initialValue = oldState[path]?.initialValue
|
||||
newFieldState.value = oldState[path].value
|
||||
|
||||
const oldErrorPaths: string[] = []
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
import { Banner } from '../../elements/Banner'
|
||||
@@ -13,6 +14,7 @@ type NullifyLocaleFieldProps = {
|
||||
localized: boolean
|
||||
path: string
|
||||
}
|
||||
|
||||
export const NullifyLocaleField: React.FC<NullifyLocaleFieldProps> = ({
|
||||
fieldValue,
|
||||
localized,
|
||||
@@ -30,8 +32,8 @@ export const NullifyLocaleField: React.FC<NullifyLocaleFieldProps> = ({
|
||||
const useFallback = !checked
|
||||
|
||||
dispatchFields({
|
||||
path,
|
||||
type: 'UPDATE',
|
||||
path,
|
||||
value: useFallback ? null : fieldValue || 0,
|
||||
})
|
||||
setModified(true)
|
||||
|
||||
18
packages/ui/src/forms/ReadOnlyProvider/index.tsx
Normal file
18
packages/ui/src/forms/ReadOnlyProvider/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
const ReadOnlyContext = React.createContext<boolean | undefined>(undefined)
|
||||
|
||||
export const ReadOnlyProvider: React.FC<{
|
||||
children: React.ReactNode
|
||||
readOnly?: boolean
|
||||
}> = (props) => {
|
||||
const { children, readOnly } = props
|
||||
|
||||
return <ReadOnlyContext.Provider value={readOnly}>{children}</ReadOnlyContext.Provider>
|
||||
}
|
||||
|
||||
export const useReadOnly = () => {
|
||||
const path = React.useContext(ReadOnlyContext)
|
||||
return path
|
||||
}
|
||||
@@ -1,20 +1,45 @@
|
||||
'use client'
|
||||
import type { FieldPermissions } from 'payload/auth'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { useOperation } from '../../providers/OperationProvider'
|
||||
import { FieldPathProvider, useFieldPath } from '../FieldPathProvider'
|
||||
import { ReadOnlyProvider, useReadOnly } from '../ReadOnlyProvider'
|
||||
|
||||
export const RenderField: React.FC<{
|
||||
Field: React.ReactNode
|
||||
fieldPermissions: FieldPermissions
|
||||
name?: string
|
||||
readOnly?: boolean
|
||||
}> = (props) => {
|
||||
const { name, Field } = props
|
||||
const { name, Field, fieldPermissions, readOnly: readOnlyFromProps } = props
|
||||
|
||||
const { path: pathFromContext, schemaPath: schemaPathFromContext } = useFieldPath()
|
||||
|
||||
const readOnlyFromContext = useReadOnly()
|
||||
|
||||
const operation = useOperation()
|
||||
|
||||
const path = `${pathFromContext ? `${pathFromContext}.` : ''}${name || ''}`
|
||||
const schemaPath = `${schemaPathFromContext ? `${schemaPathFromContext}.` : ''}${name || ''}`
|
||||
|
||||
// `admin.readOnly` displays the value but prevents the field from being edited
|
||||
let readOnly = readOnlyFromProps
|
||||
|
||||
// if parent field is `readOnly: true`, but this field is `readOnly: false`, the field should still be editable
|
||||
if (readOnlyFromContext && readOnly !== false) readOnly = true
|
||||
|
||||
// if the user does not have access control to begin with, force it to be read-only
|
||||
if (fieldPermissions?.[operation]?.permission === false) {
|
||||
readOnly = true
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldPathProvider path={path} schemaPath={schemaPath}>
|
||||
{Field}
|
||||
</FieldPathProvider>
|
||||
<ReadOnlyProvider readOnly={readOnly}>
|
||||
<FieldPathProvider path={path} schemaPath={schemaPath}>
|
||||
{Field}
|
||||
</FieldPathProvider>
|
||||
</ReadOnlyProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,8 +53,14 @@ const RenderFields: React.FC<Props> = (props) => {
|
||||
ref={intersectionRef}
|
||||
>
|
||||
{hasRendered &&
|
||||
fieldMap?.map(({ name, Field }, fieldIndex) => (
|
||||
<RenderField Field={Field} key={fieldIndex} name={name} />
|
||||
fieldMap?.map(({ name, Field, fieldPermissions, readOnly }, fieldIndex) => (
|
||||
<RenderField
|
||||
Field={Field}
|
||||
fieldPermissions={fieldPermissions}
|
||||
key={fieldIndex}
|
||||
name={name}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Operation } from 'payload/types'
|
||||
|
||||
import type { FieldMap } from '../../utilities/buildComponentMap/types'
|
||||
|
||||
export type Props = {
|
||||
@@ -5,4 +7,5 @@ export type Props = {
|
||||
fieldMap: FieldMap
|
||||
forceRender?: boolean
|
||||
margins?: 'small' | false
|
||||
operation?: Operation
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FieldPermissions } from 'payload/auth'
|
||||
import type { ArrayField, Row, RowLabel as RowLabelType } from 'payload/types'
|
||||
import type { ArrayField, Operation, Row, RowLabel as RowLabelType } from 'payload/types'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
@@ -105,10 +105,10 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
const addRow = useCallback(
|
||||
async (rowIndex: number, blockType: string) => {
|
||||
dispatchFields({
|
||||
type: 'ADD_ROW',
|
||||
blockType,
|
||||
path,
|
||||
rowIndex,
|
||||
type: 'ADD_ROW',
|
||||
})
|
||||
|
||||
setModified(true)
|
||||
@@ -117,12 +117,12 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
scrollToID(`${path}-row-${rowIndex + 1}`)
|
||||
}, 0)
|
||||
},
|
||||
[path, setModified],
|
||||
[path, setModified, dispatchFields],
|
||||
)
|
||||
|
||||
const duplicateRow = useCallback(
|
||||
(rowIndex: number) => {
|
||||
dispatchFields({ path, rowIndex, type: 'DUPLICATE_ROW' })
|
||||
dispatchFields({ type: 'DUPLICATE_ROW', path, rowIndex })
|
||||
setModified(true)
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -135,9 +135,9 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
const removeRow = useCallback(
|
||||
(rowIndex: number) => {
|
||||
dispatchFields({
|
||||
type: 'REMOVE_ROW',
|
||||
path,
|
||||
rowIndex,
|
||||
type: 'REMOVE_ROW',
|
||||
})
|
||||
|
||||
setModified(true)
|
||||
@@ -147,7 +147,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
|
||||
const moveRow = useCallback(
|
||||
(moveFromIndex: number, moveToIndex: number) => {
|
||||
dispatchFields({ moveFromIndex, moveToIndex, path, type: 'MOVE_ROW' })
|
||||
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path })
|
||||
setModified(true)
|
||||
},
|
||||
[dispatchFields, path, setModified],
|
||||
@@ -155,14 +155,14 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
|
||||
const toggleCollapseAll = useCallback(
|
||||
(collapsed: boolean) => {
|
||||
dispatchFields({ collapsed, path, setDocFieldPreferences, type: 'SET_ALL_ROWS_COLLAPSED' })
|
||||
dispatchFields({ type: 'SET_ALL_ROWS_COLLAPSED', collapsed, path, setDocFieldPreferences })
|
||||
},
|
||||
[dispatchFields, path, setDocFieldPreferences],
|
||||
)
|
||||
|
||||
const setCollapse = useCallback(
|
||||
(rowID: string, collapsed: boolean) => {
|
||||
dispatchFields({ collapsed, path, rowID, setDocFieldPreferences, type: 'SET_ROW_COLLAPSED' })
|
||||
dispatchFields({ type: 'SET_ROW_COLLAPSED', collapsed, path, rowID, setDocFieldPreferences })
|
||||
},
|
||||
[dispatchFields, path, setDocFieldPreferences],
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { DocumentPreferences } from 'payload/types'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { tabHasName } from 'payload/types'
|
||||
import { toKebabCase } from 'payload/utilities'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
|
||||
117
packages/ui/src/forms/fields/Text/Input.tsx
Normal file
117
packages/ui/src/forms/fields/Text/Input.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
import type { ChangeEvent } from 'react'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
import type { Option } from '../../../elements/ReactSelect/types'
|
||||
|
||||
import ReactSelect from '../../../elements/ReactSelect'
|
||||
import { useTranslation } from '../../../providers/Translation'
|
||||
import { type FormFieldBase, fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
export type TextInputProps = Omit<FormFieldBase, 'type'> & {
|
||||
hasMany?: boolean
|
||||
inputRef?: React.MutableRefObject<HTMLInputElement>
|
||||
maxRows?: number
|
||||
minRows?: number
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
||||
showError?: boolean
|
||||
value?: string
|
||||
valueToRender?: Option[]
|
||||
}
|
||||
|
||||
export const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
const {
|
||||
AfterInput,
|
||||
BeforeInput,
|
||||
Description,
|
||||
Error,
|
||||
Label,
|
||||
className,
|
||||
hasMany,
|
||||
inputRef,
|
||||
maxRows,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
path,
|
||||
placeholder,
|
||||
readOnly,
|
||||
rtl,
|
||||
showError,
|
||||
style,
|
||||
value,
|
||||
valueToRender,
|
||||
width,
|
||||
} = props
|
||||
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
fieldBaseClass,
|
||||
'text',
|
||||
className,
|
||||
showError && 'error',
|
||||
readOnly && 'read-only',
|
||||
hasMany && 'has-many',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
style={{
|
||||
...style,
|
||||
width,
|
||||
}}
|
||||
>
|
||||
{Error}
|
||||
{Label}
|
||||
{hasMany ? (
|
||||
<ReactSelect
|
||||
className={`field-${path.replace(/\./g, '__')}`}
|
||||
disabled={readOnly}
|
||||
// prevent adding additional options if maxRows is reached
|
||||
filterOption={() =>
|
||||
!maxRows ? true : !(Array.isArray(value) && maxRows && value.length >= maxRows)
|
||||
}
|
||||
isClearable
|
||||
isCreatable
|
||||
isMulti
|
||||
isSortable
|
||||
noOptionsMessage={() => {
|
||||
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
|
||||
if (isOverHasMany) {
|
||||
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
|
||||
}
|
||||
return null
|
||||
}}
|
||||
onChange={onChange}
|
||||
options={[]}
|
||||
placeholder={t('general:enterAValue')}
|
||||
showError={showError}
|
||||
value={valueToRender}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
{BeforeInput}
|
||||
<input
|
||||
data-rtl={rtl}
|
||||
disabled={readOnly}
|
||||
id={`field-${path?.replace(/\./g, '__')}`}
|
||||
name={path}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={value || ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
</div>
|
||||
)}
|
||||
{Description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user