diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 4f0a7b92a..ca4c1f08c 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -13,3 +13,6 @@ dfac7395fed95fc5d8ebca21b786ce70821942bb # lint and format plugin-cloud fb7d1be2f3325d076b7c967b1730afcef37922c2 + +# lint and format create-payload-app +5fd3d430001efe86515262ded5e26f00c1451181 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a6b947549..b1bec3778 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -214,6 +214,7 @@ jobs: matrix: pkg: - plugin-cloud + - create-payload-app steps: - name: Use Node.js 18 @@ -238,3 +239,4 @@ jobs: - name: Test ${{ matrix.pkg }} run: pnpm --filter ${{ matrix.pkg }} run test + if: matrix.pkg != 'create-payload-app' # degit doesn't work within GitHub Actions diff --git a/docs/admin/components.mdx b/docs/admin/components.mdx index 107f73edc..5cee9d99f 100644 --- a/docs/admin/components.mdx +++ b/docs/admin/components.mdx @@ -129,7 +129,7 @@ To add a _new_ view to the Admin Panel, simply add another key to the `views` ob } ``` -_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/master/test/admin/components)._ +_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/main/test/admin/components)._ For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component). @@ -399,12 +399,12 @@ Your custom view components will be given all the props that a React Router `(props: { depth?: number @@ -127,13 +133,18 @@ export const useLivePreview = (props: { const { depth = 0, initialData, serverURL } = props const [data, setData] = useState(initialData) const [isLoading, setIsLoading] = useState(true) + const hasSentReadyMessage = useRef(false) const onChange = useCallback((mergedData) => { + // When a change is made, the `onChange` callback will be called with the merged data + // Set this merged data into state so that React will re-render the UI setData(mergedData) setIsLoading(false) }, []) useEffect(() => { + // Listen for `window.postMessage` events from the Admin panel + // When a change is made, the `onChange` callback will be called with the merged data const subscription = subscribe({ callback: onChange, depth, @@ -141,6 +152,17 @@ export const useLivePreview = (props: { serverURL, }) + // Once subscribed, send a `ready` message back up to the Admin panel + // This will indicate that the front-end is ready to receive messages + if (!hasSentReadyMessage.current) { + hasSentReadyMessage.current = true + + ready({ + serverURL + }) + } + + // When the component unmounts, unsubscribe from the `window.postMessage` events return () => { unsubscribe(subscription) } diff --git a/docs/live-preview/overview.mdx b/docs/live-preview/overview.mdx index c0006226e..a33b330b2 100644 --- a/docs/live-preview/overview.mdx +++ b/docs/live-preview/overview.mdx @@ -85,7 +85,7 @@ Here is an example of using a function that returns a dynamic URL: locale }) => `${data.tenant.url}${ // Multi-tenant top-level domain documentInfo.slug === 'posts' ? `/posts/${data.slug}` : `${data.slug !== 'home' : `/${data.slug}` : ''}` - `}?locale=${locale}`, // Localization query param + }${locale ? `?locale=${locale?.code}` : ''}`, // Localization query param collections: ['pages'], }, } diff --git a/package.json b/package.json index 1a57c2fd1..64dea8217 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "lint-staged": "lint-staged", "pretest": "pnpm build", "reinstall": "pnpm clean:unix && pnpm install", - "list:packages": "./scripts/list_published_packages.sh beta", - "script:release:beta": "./scripts/release_beta.sh", + "script:list-packages": "tsx ./scripts/list-packages.ts", + "script:release": "tsx ./scripts/release.ts", "test": "pnpm test:int && pnpm test:components && pnpm test:e2e", "test:components": "cross-env jest --config=jest.components.config.js", "test:e2e": "npx playwright install --with-deps && ts-node -T ./test/runE2E.ts", @@ -74,6 +74,7 @@ "qs": "6.11.2", "rimraf": "3.0.2", "shelljs": "0.8.5", + "simple-git": "^3.20.0", "slash": "3.0.0", "slate": "0.91.4", "ts-node": "10.9.1", diff --git a/packages/create-payload-app/.eslintrc.js b/packages/create-payload-app/.eslintrc.js new file mode 100644 index 000000000..4367ff5ca --- /dev/null +++ b/packages/create-payload-app/.eslintrc.js @@ -0,0 +1,44 @@ +/** @type {import('prettier').Config} */ +module.exports = { + extends: ['@payloadcms'], + ignorePatterns: ['README.md', '**/*.spec.ts'], + overrides: [ + { + extends: ['plugin:@typescript-eslint/disable-type-checked'], + files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'], + }, + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + 'no-console': 'off', + }, + }, + { + files: ['package.json', 'tsconfig.json'], + rules: { + 'perfectionist/sort-array-includes': 'off', + 'perfectionist/sort-astro-attributes': 'off', + 'perfectionist/sort-classes': 'off', + 'perfectionist/sort-enums': 'off', + 'perfectionist/sort-exports': 'off', + 'perfectionist/sort-imports': 'off', + 'perfectionist/sort-interfaces': 'off', + 'perfectionist/sort-jsx-props': 'off', + 'perfectionist/sort-keys': 'off', + 'perfectionist/sort-maps': 'off', + 'perfectionist/sort-named-exports': 'off', + 'perfectionist/sort-named-imports': 'off', + 'perfectionist/sort-object-types': 'off', + 'perfectionist/sort-objects': 'off', + 'perfectionist/sort-svelte-attributes': 'off', + 'perfectionist/sort-union-types': 'off', + 'perfectionist/sort-vue-attributes': 'off', + }, + }, + ], + parserOptions: { + project: ['./tsconfig.json'], + tsconfigRootDir: __dirname, + }, + root: true, +} diff --git a/packages/create-payload-app/README.md b/packages/create-payload-app/README.md new file mode 100644 index 000000000..ff32b81e6 --- /dev/null +++ b/packages/create-payload-app/README.md @@ -0,0 +1,34 @@ +# Create Payload App + +CLI for easily starting new Payload project + +## Usage + +```text + + USAGE + + $ npx create-payload-app + $ npx create-payload-app my-project + $ npx create-payload-app -n my-project -t blog + + OPTIONS + + -n my-payload-app Set project name + -t template_name Choose specific template + + Available templates: + + blank Blank Template + website Website Template + ecommerce E-commerce Template + plugin Template for creating a Payload plugin + payload-demo Payload demo site at https://demo.payloadcms.com + payload-website Payload website CMS at https://payloadcms.com + + --use-npm Use npm to install dependencies + --use-yarn Use yarn to install dependencies + --use-pnpm Use pnpm to install dependencies + --no-deps Do not install any dependencies + -h Show help +``` diff --git a/packages/create-payload-app/bin/cli.js b/packages/create-payload-app/bin/cli.js new file mode 100755 index 000000000..30f38aa97 --- /dev/null +++ b/packages/create-payload-app/bin/cli.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../dist/index.js') diff --git a/packages/create-payload-app/jest.config.js b/packages/create-payload-app/jest.config.js new file mode 100644 index 000000000..b27ed1519 --- /dev/null +++ b/packages/create-payload-app/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + testEnvironment: 'node', + testMatch: ['**/src/**/?(*.)+(spec|test|it-test).[tj]s?(x)'], + testTimeout: 10000, + transform: { + '^.+\\.(ts|tsx)?$': 'ts-jest', + }, + verbose: true, +} diff --git a/packages/create-payload-app/package.json b/packages/create-payload-app/package.json new file mode 100644 index 000000000..487ea3377 --- /dev/null +++ b/packages/create-payload-app/package.json @@ -0,0 +1,47 @@ +{ + "name": "create-payload-app", + "version": "0.5.2", + "license": "MIT", + "bin": { + "create-payload-app": "bin/cli.js" + }, + "scripts": { + "build": "tsc && pnpm copyfiles", + "copyfiles": "copyfiles -u 1 \"src/templates/**\" \"src/lib/common-files/**\" dist", + "clean": "rimraf dist", + "typecheck": "tsc --noEmit", + "lint": "eslint \"src/**/*.ts\"", + "lint:fix": "eslint \"src/**/*.ts\" --fix", + "lint-staged": "lint-staged --quiet", + "test": "jest", + "prepublishOnly": "pnpm test && pnpm clean && pnpm build" + }, + "files": [ + "package.json", + "dist", + "bin" + ], + "dependencies": { + "@sindresorhus/slugify": "^1.1.0", + "arg": "^5.0.0", + "chalk": "^4.1.0", + "command-exists": "^1.2.9", + "degit": "^2.8.4", + "execa": "^5.0.0", + "figures": "^3.2.0", + "fs-extra": "^9.0.1", + "handlebars": "^4.7.7", + "ora": "^5.1.0", + "prompts": "^2.4.2", + "terminal-link": "^2.1.1" + }, + "devDependencies": { + "@types/command-exists": "^1.2.0", + "@types/degit": "^2.8.3", + "@types/fs-extra": "^9.0.12", + "@types/jest": "^27.0.3", + "@types/node": "^16.6.2", + "@types/prompts": "^2.4.1", + "ts-jest": "^29.1.0" + } +} diff --git a/packages/create-payload-app/src/index.ts b/packages/create-payload-app/src/index.ts new file mode 100644 index 000000000..c5e28d32e --- /dev/null +++ b/packages/create-payload-app/src/index.ts @@ -0,0 +1,8 @@ +import { Main } from './main' +import { error } from './utils/log' + +async function main(): Promise { + await new Main().init() +} + +main().catch((e) => error(`An error has occurred: ${e instanceof Error ? e.message : e}`)) diff --git a/packages/create-payload-app/src/lib/configure-payload-config.ts b/packages/create-payload-app/src/lib/configure-payload-config.ts new file mode 100644 index 000000000..d710a8420 --- /dev/null +++ b/packages/create-payload-app/src/lib/configure-payload-config.ts @@ -0,0 +1,117 @@ +import fse from 'fs-extra' +import path from 'path' + +import type { DbDetails } from '../types' + +import { warning } from '../utils/log' +import { bundlerPackages, dbPackages, editorPackages } from './packages' + +/** Update payload config with necessary imports and adapters */ +export async function configurePayloadConfig(args: { + dbDetails: DbDetails | undefined + projectDir: string +}): Promise { + if (!args.dbDetails) { + return + } + + // Update package.json + const packageJsonPath = path.resolve(args.projectDir, 'package.json') + try { + const packageObj = await fse.readJson(packageJsonPath) + + packageObj.dependencies['payload'] = '^2.0.0' + + const dbPackage = dbPackages[args.dbDetails.type] + const bundlerPackage = bundlerPackages['webpack'] + const editorPackage = editorPackages['slate'] + + // Delete all other db adapters + Object.values(dbPackages).forEach((p) => { + if (p.packageName !== dbPackage.packageName) { + delete packageObj.dependencies[p.packageName] + } + }) + + packageObj.dependencies[dbPackage.packageName] = dbPackage.version + packageObj.dependencies[bundlerPackage.packageName] = bundlerPackage.version + packageObj.dependencies[editorPackage.packageName] = editorPackage.version + + await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 }) + } catch (err: unknown) { + warning('Unable to update name in package.json') + } + + try { + const possiblePaths = [ + path.resolve(args.projectDir, 'src/payload.config.ts'), + path.resolve(args.projectDir, 'src/payload/payload.config.ts'), + ] + + let payloadConfigPath: string | undefined + + possiblePaths.forEach((p) => { + if (fse.pathExistsSync(p) && !payloadConfigPath) { + payloadConfigPath = p + } + }) + + if (!payloadConfigPath) { + warning('Unable to update payload.config.ts with plugins') + return + } + + const configContent = fse.readFileSync(payloadConfigPath, 'utf-8') + const configLines = configContent.split('\n') + + const dbReplacement = dbPackages[args.dbDetails.type] + const bundlerReplacement = bundlerPackages['webpack'] + const editorReplacement = editorPackages['slate'] + + let dbConfigStartLineIndex: number | undefined + let dbConfigEndLineIndex: number | undefined + + configLines.forEach((l, i) => { + if (l.includes('// database-adapter-import')) { + configLines[i] = dbReplacement.importReplacement + } + if (l.includes('// bundler-import')) { + configLines[i] = bundlerReplacement.importReplacement + } + + if (l.includes('// bundler-config')) { + configLines[i] = bundlerReplacement.configReplacement + } + + if (l.includes('// editor-import')) { + configLines[i] = editorReplacement.importReplacement + } + + if (l.includes('// editor-config')) { + configLines[i] = editorReplacement.configReplacement + } + + if (l.includes('// database-adapter-config-start')) { + dbConfigStartLineIndex = i + } + if (l.includes('// database-adapter-config-end')) { + dbConfigEndLineIndex = i + } + }) + + if (!dbConfigStartLineIndex || !dbConfigEndLineIndex) { + warning('Unable to update payload.config.ts with database adapter import') + } else { + // Replaces lines between `// database-adapter-config-start` and `// database-adapter-config-end` + configLines.splice( + dbConfigStartLineIndex, + dbConfigEndLineIndex - dbConfigStartLineIndex + 1, + ...dbReplacement.configReplacement, + ) + } + + fse.writeFileSync(payloadConfigPath, configLines.join('\n')) + } catch (err: unknown) { + warning('Unable to update payload.config.ts with plugins') + } +} diff --git a/packages/create-payload-app/src/lib/create-project.spec.ts b/packages/create-payload-app/src/lib/create-project.spec.ts new file mode 100644 index 000000000..4ee8f1c96 --- /dev/null +++ b/packages/create-payload-app/src/lib/create-project.spec.ts @@ -0,0 +1,151 @@ +import fse from 'fs-extra' +import path from 'path' +import type { BundlerType, CliArgs, DbType, ProjectTemplate } from '../types' +import { createProject } from './create-project' +import { bundlerPackages, dbPackages, editorPackages } from './packages' +import exp from 'constants' + +const projectDir = path.resolve(__dirname, './tmp') +describe('createProject', () => { + beforeAll(() => { + console.log = jest.fn() + }) + + beforeEach(() => { + if (fse.existsSync(projectDir)) { + fse.rmdirSync(projectDir, { recursive: true }) + } + }) + afterEach(() => { + if (fse.existsSync(projectDir)) { + fse.rmSync(projectDir, { recursive: true }) + } + }) + + describe('#createProject', () => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const args = { + _: ['project-name'], + '--db': 'mongodb', + '--no-deps': true, + } as CliArgs + const packageManager = 'yarn' + + it('creates starter project', async () => { + const projectName = 'starter-project' + const template: ProjectTemplate = { + name: 'blank', + type: 'starter', + url: 'https://github.com/payloadcms/payload/templates/blank', + description: 'Blank Template', + } + await createProject({ + cliArgs: args, + projectName, + projectDir, + template, + packageManager, + }) + + const packageJsonPath = path.resolve(projectDir, 'package.json') + const packageJson = fse.readJsonSync(packageJsonPath) + + // Check package name and description + expect(packageJson.name).toEqual(projectName) + }) + + it('creates plugin template', async () => { + const projectName = 'plugin' + const template: ProjectTemplate = { + name: 'plugin', + type: 'plugin', + url: 'https://github.com/payloadcms/payload-plugin-template', + description: 'Template for creating a Payload plugin', + } + await createProject({ + cliArgs: args, + projectName, + projectDir, + template, + packageManager, + }) + + const packageJsonPath = path.resolve(projectDir, 'package.json') + const packageJson = fse.readJsonSync(packageJsonPath) + + // Check package name and description + expect(packageJson.name).toEqual(projectName) + }) + + describe('db adapters and bundlers', () => { + it.each([ + ['mongodb', 'webpack'], + ['postgres', 'webpack'], + ])('update config and deps: %s, %s', async (db, bundler) => { + const projectName = 'starter-project' + const template: ProjectTemplate = { + name: 'blank', + type: 'starter', + url: 'https://github.com/payloadcms/payload/templates/blank', + description: 'Blank Template', + } + await createProject({ + cliArgs: args, + projectName, + projectDir, + template, + packageManager, + dbDetails: { + dbUri: `${db}://localhost:27017/create-project-test`, + type: db as DbType, + }, + }) + + const dbReplacement = dbPackages[db as DbType] + const bundlerReplacement = bundlerPackages[bundler as BundlerType] + const editorReplacement = editorPackages['slate'] + + const packageJsonPath = path.resolve(projectDir, 'package.json') + const packageJson = fse.readJsonSync(packageJsonPath) + + // Check deps + expect(packageJson.dependencies['payload']).toEqual('^2.0.0') + expect(packageJson.dependencies[dbReplacement.packageName]).toEqual(dbReplacement.version) + + // Should only have one db adapter + expect( + Object.keys(packageJson.dependencies).filter((n) => n.startsWith('@payloadcms/db-')), + ).toHaveLength(1) + + expect(packageJson.dependencies[bundlerReplacement.packageName]).toEqual( + bundlerReplacement.version, + ) + expect(packageJson.dependencies[editorReplacement.packageName]).toEqual( + editorReplacement.version, + ) + + const payloadConfigPath = path.resolve(projectDir, 'src/payload.config.ts') + const content = fse.readFileSync(payloadConfigPath, 'utf-8') + + // Check payload.config.ts + expect(content).not.toContain('// database-adapter-import') + expect(content).toContain(dbReplacement.importReplacement) + + expect(content).not.toContain('// database-adapter-config-start') + expect(content).not.toContain('// database-adapter-config-end') + expect(content).toContain(dbReplacement.configReplacement.join('\n')) + + expect(content).not.toContain('// bundler-config-import') + expect(content).toContain(bundlerReplacement.importReplacement) + + expect(content).not.toContain('// bundler-config') + expect(content).toContain(bundlerReplacement.configReplacement) + }) + }) + }) + + describe('Templates', () => { + it.todo('Verify that all templates are valid') + // Loop through all templates.ts that should have replacement comments, and verify that they are present + }) +}) diff --git a/packages/create-payload-app/src/lib/create-project.ts b/packages/create-payload-app/src/lib/create-project.ts new file mode 100644 index 000000000..554cb1207 --- /dev/null +++ b/packages/create-payload-app/src/lib/create-project.ts @@ -0,0 +1,102 @@ +import chalk from 'chalk' +import degit from 'degit' +import execa from 'execa' +import fse from 'fs-extra' +import ora from 'ora' +import path from 'path' + +import type { CliArgs, DbDetails, PackageManager, ProjectTemplate } from '../types' + +import { error, success, warning } from '../utils/log' +import { configurePayloadConfig } from './configure-payload-config' + +async function createOrFindProjectDir(projectDir: string): Promise { + const pathExists = await fse.pathExists(projectDir) + if (!pathExists) { + await fse.mkdir(projectDir) + } +} + +async function installDeps(args: { + cliArgs: CliArgs + packageManager: PackageManager + projectDir: string +}): Promise { + const { cliArgs, packageManager, projectDir } = args + if (cliArgs['--no-deps']) { + return true + } + let installCmd = 'npm install --legacy-peer-deps' + + if (packageManager === 'yarn') { + installCmd = 'yarn' + } else if (packageManager === 'pnpm') { + installCmd = 'pnpm install' + } + + try { + await execa.command(installCmd, { + cwd: path.resolve(projectDir), + }) + return true + } catch (err: unknown) { + console.log({ err }) + return false + } +} + +export async function createProject(args: { + cliArgs: CliArgs + dbDetails?: DbDetails + packageManager: PackageManager + projectDir: string + projectName: string + template: ProjectTemplate +}): Promise { + const { cliArgs, dbDetails, packageManager, projectDir, projectName, template } = args + + await createOrFindProjectDir(projectDir) + + console.log(`\n Creating project in ${chalk.green(path.resolve(projectDir))}\n`) + + if ('url' in template) { + const emitter = degit(template.url) + await emitter.clone(projectDir) + } + + const spinner = ora('Checking latest Payload version...').start() + + await updatePackageJSON({ projectDir, projectName }) + await configurePayloadConfig({ dbDetails, projectDir }) + + // Remove yarn.lock file. This is only desired in Payload Cloud. + const lockPath = path.resolve(projectDir, 'yarn.lock') + if (fse.existsSync(lockPath)) { + await fse.remove(lockPath) + } + + spinner.text = 'Installing dependencies...' + const result = await installDeps({ cliArgs, packageManager, projectDir }) + spinner.stop() + spinner.clear() + if (result) { + success('Dependencies installed') + } else { + error('Error installing dependencies') + } +} + +export async function updatePackageJSON(args: { + projectDir: string + projectName: string +}): Promise { + const { projectDir, projectName } = args + const packageJsonPath = path.resolve(projectDir, 'package.json') + try { + const packageObj = await fse.readJson(packageJsonPath) + packageObj.name = projectName + await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 }) + } catch (err: unknown) { + warning('Unable to update name in package.json') + } +} diff --git a/packages/create-payload-app/src/lib/generate-secret.ts b/packages/create-payload-app/src/lib/generate-secret.ts new file mode 100644 index 000000000..ce503c811 --- /dev/null +++ b/packages/create-payload-app/src/lib/generate-secret.ts @@ -0,0 +1,5 @@ +import { randomBytes } from 'crypto' + +export function generateSecret(): string { + return randomBytes(32).toString('hex').slice(0, 24) +} diff --git a/packages/create-payload-app/src/lib/packages.ts b/packages/create-payload-app/src/lib/packages.ts new file mode 100644 index 000000000..2c936e929 --- /dev/null +++ b/packages/create-payload-app/src/lib/packages.ts @@ -0,0 +1,83 @@ +import type { BundlerType, DbType, EditorType } from '../types' + +type DbAdapterReplacement = { + configReplacement: string[] + importReplacement: string + packageName: string + version: string +} + +type BundlerReplacement = { + configReplacement: string + importReplacement: string + packageName: string + version: string +} + +type EditorReplacement = { + configReplacement: string + importReplacement: string + packageName: string + version: string +} + +const mongodbReplacement: DbAdapterReplacement = { + importReplacement: "import { mongooseAdapter } from '@payloadcms/db-mongodb'", + packageName: '@payloadcms/db-mongodb', + // Replacement between `// database-adapter-config-start` and `// database-adapter-config-end` + configReplacement: [' db: mongooseAdapter({', ' url: process.env.DATABASE_URI,', ' }),'], + version: '^1.0.0', +} + +const postgresReplacement: DbAdapterReplacement = { + configReplacement: [ + ' db: postgresAdapter({', + ' pool: {', + ' connectionString: process.env.DATABASE_URI,', + ' },', + ' }),', + ], + importReplacement: "import { postgresAdapter } from '@payloadcms/db-postgres'", + packageName: '@payloadcms/db-postgres', + version: '^0.x', // up to, not including 1.0.0 +} + +export const dbPackages: Record = { + mongodb: mongodbReplacement, + postgres: postgresReplacement, +} + +const webpackReplacement: BundlerReplacement = { + importReplacement: "import { webpackBundler } from '@payloadcms/bundler-webpack'", + packageName: '@payloadcms/bundler-webpack', + // Replacement of line containing `// bundler-config` + configReplacement: ' bundler: webpackBundler(),', + version: '^1.0.0', +} + +const viteReplacement: BundlerReplacement = { + configReplacement: ' bundler: viteBundler(),', + importReplacement: "import { viteBundler } from '@payloadcms/bundler-vite'", + packageName: '@payloadcms/bundler-vite', + version: '^0.x', // up to, not including 1.0.0 +} + +export const bundlerPackages: Record = { + vite: viteReplacement, + webpack: webpackReplacement, +} + +export const editorPackages: Record = { + lexical: { + configReplacement: ' editor: lexicalEditor({}),', + importReplacement: "import { lexicalEditor } from '@payloadcms/richtext-lexical'", + packageName: '@payloadcms/richtext-lexical', + version: '^0.x', // up to, not including 1.0.0 + }, + slate: { + configReplacement: ' editor: slateEditor({}),', + importReplacement: "import { slateEditor } from '@payloadcms/richtext-slate'", + packageName: '@payloadcms/richtext-slate', + version: '^1.0.0', + }, +} diff --git a/packages/create-payload-app/src/lib/parse-project-name.ts b/packages/create-payload-app/src/lib/parse-project-name.ts new file mode 100644 index 000000000..be01c0582 --- /dev/null +++ b/packages/create-payload-app/src/lib/parse-project-name.ts @@ -0,0 +1,24 @@ +import prompts from 'prompts' + +import type { CliArgs } from '../types' + +export async function parseProjectName(args: CliArgs): Promise { + if (args['--name']) return args['--name'] + if (args._[0]) return args._[0] + + const response = await prompts( + { + name: 'value', + message: 'Project name?', + type: 'text', + validate: (value: string) => !!value.length, + }, + { + onCancel: () => { + process.exit(0) + }, + }, + ) + + return response.value +} diff --git a/packages/create-payload-app/src/lib/parse-template.ts b/packages/create-payload-app/src/lib/parse-template.ts new file mode 100644 index 000000000..845ef114b --- /dev/null +++ b/packages/create-payload-app/src/lib/parse-template.ts @@ -0,0 +1,41 @@ +import prompts from 'prompts' + +import type { CliArgs, ProjectTemplate } from '../types' + +export async function parseTemplate( + args: CliArgs, + validTemplates: ProjectTemplate[], +): Promise { + if (args['--template']) { + const templateName = args['--template'] + const template = validTemplates.find((t) => t.name === templateName) + if (!template) throw new Error('Invalid template given') + return template + } + + const response = await prompts( + { + name: 'value', + choices: validTemplates.map((p) => { + return { + description: p.description, + title: p.name, + value: p.name, + } + }), + message: 'Choose project template', + type: 'select', + validate: (value: string) => !!value.length, + }, + { + onCancel: () => { + process.exit(0) + }, + }, + ) + + const template = validTemplates.find((t) => t.name === response.value) + if (!template) throw new Error('Template is undefined') + + return template +} diff --git a/packages/create-payload-app/src/lib/select-db.ts b/packages/create-payload-app/src/lib/select-db.ts new file mode 100644 index 000000000..0bd68333c --- /dev/null +++ b/packages/create-payload-app/src/lib/select-db.ts @@ -0,0 +1,86 @@ +import slugify from '@sindresorhus/slugify' +import prompts from 'prompts' + +import type { CliArgs, DbDetails, DbType } from '../types' + +type DbChoice = { + dbConnectionPrefix: `${string}/` + title: string + value: DbType +} + +const dbChoiceRecord: Record = { + mongodb: { + dbConnectionPrefix: 'mongodb://127.0.0.1/', + title: 'MongoDB', + value: 'mongodb', + }, + postgres: { + dbConnectionPrefix: 'postgres://127.0.0.1:5432/', + title: 'PostgreSQL (beta)', + value: 'postgres', + }, +} + +export async function selectDb(args: CliArgs, projectName: string): Promise { + let dbType: DbType | undefined = undefined + if (args['--db']) { + if (!Object.values(dbChoiceRecord).some((dbChoice) => dbChoice.value === args['--db'])) { + throw new Error( + `Invalid database type given. Valid types are: ${Object.values(dbChoiceRecord) + .map((dbChoice) => dbChoice.value) + .join(', ')}`, + ) + } + dbType = args['--db'] as DbType + } else { + const dbTypeRes = await prompts( + { + name: 'value', + choices: Object.values(dbChoiceRecord).map((dbChoice) => { + return { + title: dbChoice.title, + value: dbChoice.value, + } + }), + message: 'Select a database', + type: 'select', + validate: (value: string) => !!value.length, + }, + { + onCancel: () => { + process.exit(0) + }, + }, + ) + dbType = dbTypeRes.value + } + + const dbChoice = dbChoiceRecord[dbType] + + const dbUriRes = await prompts( + { + name: 'value', + initial: `${dbChoice.dbConnectionPrefix}${ + projectName === '.' ? `payload-${getRandomDigitSuffix()}` : slugify(projectName) + }`, + message: `Enter ${dbChoice.title.split(' ')[0]} connection string`, // strip beta from title + type: 'text', + validate: (value: string) => !!value.length, + }, + { + onCancel: () => { + process.exit(0) + }, + }, + ) + + return { + dbUri: dbUriRes.value, + type: dbChoice.value, + } +} + +function getRandomDigitSuffix(): string { + return (Math.random() * Math.pow(10, 6)).toFixed(0) +} diff --git a/packages/create-payload-app/src/lib/templates.ts b/packages/create-payload-app/src/lib/templates.ts new file mode 100644 index 000000000..b38db7e9f --- /dev/null +++ b/packages/create-payload-app/src/lib/templates.ts @@ -0,0 +1,54 @@ +import type { ProjectTemplate } from '../types' + +import { error, info } from '../utils/log' + +export function validateTemplate(templateName: string): boolean { + const validTemplates = getValidTemplates() + if (!validTemplates.map((t) => t.name).includes(templateName)) { + error(`'${templateName}' is not a valid template.`) + info(`Valid templates: ${validTemplates.map((t) => t.name).join(', ')}`) + return false + } + return true +} + +export function getValidTemplates(): ProjectTemplate[] { + return [ + { + name: 'blank', + description: 'Blank Template', + type: 'starter', + url: 'https://github.com/payloadcms/payload/templates/blank', + }, + { + name: 'website', + description: 'Website Template', + type: 'starter', + url: 'https://github.com/payloadcms/payload/templates/website', + }, + { + name: 'ecommerce', + description: 'E-commerce Template', + type: 'starter', + url: 'https://github.com/payloadcms/payload/templates/ecommerce', + }, + { + name: 'plugin', + description: 'Template for creating a Payload plugin', + type: 'plugin', + url: 'https://github.com/payloadcms/payload-plugin-template', + }, + { + name: 'payload-demo', + description: 'Payload demo site at https://demo.payloadcms.com', + type: 'starter', + url: 'https://github.com/payloadcms/public-demo', + }, + { + name: 'payload-website', + description: 'Payload website CMS at https://payloadcms.com', + type: 'starter', + url: 'https://github.com/payloadcms/website-cms', + }, + ] +} diff --git a/packages/create-payload-app/src/lib/write-env-file.ts b/packages/create-payload-app/src/lib/write-env-file.ts new file mode 100644 index 000000000..566c98af1 --- /dev/null +++ b/packages/create-payload-app/src/lib/write-env-file.ts @@ -0,0 +1,55 @@ +import fs from 'fs-extra' +import path from 'path' + +import type { ProjectTemplate } from '../types' + +import { error, success } from '../utils/log' + +/** Parse and swap .env.example values and write .env */ +export async function writeEnvFile(args: { + databaseUri: string + payloadSecret: string + projectDir: string + template: ProjectTemplate +}): Promise { + const { databaseUri, payloadSecret, projectDir, template } = args + try { + if (template.type === 'starter' && fs.existsSync(path.join(projectDir, '.env.example'))) { + // Parse .env file into key/value pairs + const envFile = await fs.readFile(path.join(projectDir, '.env.example'), 'utf8') + const envWithValues: string[] = envFile + .split('\n') + .filter((e) => e) + .map((line) => { + if (line.startsWith('#') || !line.includes('=')) return line + + const split = line.split('=') + const key = split[0] + let value = split[1] + + if (key === 'MONGODB_URI' || key === 'MONGO_URL' || key === 'DATABASE_URI') { + value = databaseUri + } + if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') { + value = payloadSecret + } + + return `${key}=${value}` + }) + + // Write new .env file + await fs.writeFile(path.join(projectDir, '.env'), envWithValues.join('\n')) + } else { + const content = `MONGODB_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}` + await fs.outputFile(`${projectDir}/.env`, content) + } + + success('.env file created') + } catch (err: unknown) { + error('Unable to write .env file') + if (err instanceof Error) { + error(err.message) + } + process.exit(1) + } +} diff --git a/packages/create-payload-app/src/main.ts b/packages/create-payload-app/src/main.ts new file mode 100644 index 000000000..1a37afe83 --- /dev/null +++ b/packages/create-payload-app/src/main.ts @@ -0,0 +1,133 @@ +import slugify from '@sindresorhus/slugify' +import arg from 'arg' +import commandExists from 'command-exists' + +import type { CliArgs, PackageManager } from './types' + +import { createProject } from './lib/create-project' +import { generateSecret } from './lib/generate-secret' +import { parseProjectName } from './lib/parse-project-name' +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 { helpMessage, successMessage, welcomeMessage } from './utils/messages' + +export class Main { + args: CliArgs + + constructor() { + // @ts-expect-error bad typings + this.args = arg( + { + '--db': String, + '--help': Boolean, + '--name': String, + '--secret': String, + '--template': String, + + // Package manager + '--no-deps': Boolean, + '--use-npm': Boolean, + '--use-pnpm': Boolean, + '--use-yarn': Boolean, + + // Flags + '--beta': Boolean, + '--dry-run': Boolean, + + // Aliases + '-d': '--db', + '-h': '--help', + '-n': '--name', + '-t': '--template', + }, + { permissive: true }, + ) + } + + async init(): Promise { + try { + if (this.args['--help']) { + console.log(helpMessage()) + process.exit(0) + } + const templateArg = this.args['--template'] + if (templateArg) { + const valid = validateTemplate(templateArg) + if (!valid) { + console.log(helpMessage()) + process.exit(1) + } + } + + console.log(welcomeMessage) + const projectName = await parseProjectName(this.args) + const validTemplates = getValidTemplates() + const template = await parseTemplate(this.args, validTemplates) + + const projectDir = projectName === '.' ? process.cwd() : `./${slugify(projectName)}` + const packageManager = await getPackageManager(this.args) + + if (template.type !== 'plugin') { + const dbDetails = await selectDb(this.args, projectName) + const payloadSecret = generateSecret() + if (!this.args['--dry-run']) { + await createProject({ + cliArgs: this.args, + dbDetails, + packageManager, + projectDir, + projectName, + template, + }) + await writeEnvFile({ + databaseUri: dbDetails.dbUri, + payloadSecret, + projectDir, + template, + }) + } + } else { + if (!this.args['--dry-run']) { + await createProject({ + cliArgs: this.args, + packageManager, + projectDir, + projectName, + template, + }) + } + } + + success('Payload project successfully created') + console.log(successMessage(projectDir, packageManager)) + } catch (error: unknown) { + console.log(error) + } + } +} + +async function getPackageManager(args: CliArgs): Promise { + let packageManager: PackageManager = 'npm' + + if (args['--use-npm']) { + packageManager = 'npm' + } else if (args['--use-yarn']) { + packageManager = 'yarn' + } else if (args['--use-pnpm']) { + packageManager = 'pnpm' + } else { + try { + if (await commandExists('yarn')) { + packageManager = 'yarn' + } else if (await commandExists('pnpm')) { + packageManager = 'pnpm' + } + } catch (error: unknown) { + packageManager = 'npm' + } + } + return packageManager +} diff --git a/packages/create-payload-app/src/types.ts b/packages/create-payload-app/src/types.ts new file mode 100644 index 000000000..c981e18fb --- /dev/null +++ b/packages/create-payload-app/src/types.ts @@ -0,0 +1,58 @@ +import type arg from 'arg' + +export interface Args extends arg.Spec { + '--beta': BooleanConstructor + '--db': StringConstructor + '--dry-run': BooleanConstructor + '--help': BooleanConstructor + '--name': StringConstructor + '--no-deps': BooleanConstructor + '--secret': StringConstructor + '--template': StringConstructor + '--use-npm': BooleanConstructor + '--use-pnpm': BooleanConstructor + '--use-yarn': BooleanConstructor + '-h': string + '-n': string + '-t': string +} + +export type CliArgs = arg.Result + +export type ProjectTemplate = GitTemplate | PluginTemplate + +/** + * Template that is cloned verbatim from a git repo + * Performs .env manipulation based upon input + */ +export interface GitTemplate extends Template { + type: 'starter' + url: string +} + +/** + * Type specifically for the plugin template + * No .env manipulation is done + */ +export interface PluginTemplate extends Template { + type: 'plugin' + url: string +} + +interface Template { + description?: string + name: string + type: ProjectTemplate['type'] +} + +export type PackageManager = 'npm' | 'pnpm' | 'yarn' + +export type DbType = 'mongodb' | 'postgres' + +export type DbDetails = { + dbUri: string + type: DbType +} + +export type BundlerType = 'vite' | 'webpack' +export type EditorType = 'lexical' | 'slate' diff --git a/packages/create-payload-app/src/utils/log.ts b/packages/create-payload-app/src/utils/log.ts new file mode 100644 index 000000000..eb3459a21 --- /dev/null +++ b/packages/create-payload-app/src/utils/log.ts @@ -0,0 +1,18 @@ +import chalk from 'chalk' +import figures from 'figures' + +export const success = (message: string): void => { + console.log(`${chalk.green(figures.tick)} ${chalk.bold(message)}`) +} + +export const warning = (message: string): void => { + console.log(chalk.yellow('? ') + chalk.bold(message)) +} + +export const info = (message: string): void => { + console.log(`${chalk.yellow(figures.info)} ${chalk.bold(message)}`) +} + +export const error = (message: string): void => { + console.log(`${chalk.red(figures.cross)} ${chalk.bold(message)}`) +} diff --git a/packages/create-payload-app/src/utils/messages.ts b/packages/create-payload-app/src/utils/messages.ts new file mode 100644 index 000000000..dd4f27538 --- /dev/null +++ b/packages/create-payload-app/src/utils/messages.ts @@ -0,0 +1,76 @@ +import chalk from 'chalk' +import figures from 'figures' +import path from 'path' +import terminalLink from 'terminal-link' + +import type { ProjectTemplate } from '../types' + +import { getValidTemplates } from '../lib/templates' + +const header = (message: string): string => `${chalk.yellow(figures.star)} ${chalk.bold(message)}` + +export const welcomeMessage = chalk` + {green Welcome to Payload. Let's create a project! } +` + +const spacer = ' '.repeat(8) + +export function helpMessage(): string { + const validTemplates = getValidTemplates() + return chalk` + {bold USAGE} + + {dim $} {bold npx create-payload-app} + {dim $} {bold npx create-payload-app} my-project + {dim $} {bold npx create-payload-app} -n my-project -t blog + + {bold OPTIONS} + + -n {underline my-payload-app} Set project name + -t {underline template_name} Choose specific template + + {dim Available templates: ${formatTemplates(validTemplates)}} + + --use-npm Use npm to install dependencies + --use-yarn Use yarn to install dependencies + --use-pnpm Use pnpm to install dependencies + --no-deps Do not install any dependencies + -h Show help +` +} + +function formatTemplates(templates: ProjectTemplate[]) { + return `\n\n${spacer}${templates + .map((t) => `${t.name}${' '.repeat(28 - t.name.length)}${t.description}`) + .join(`\n${spacer}`)}` +} + +export function successMessage(projectDir: string, packageManager: string): string { + return ` + ${header('Launch Application:')} + + - cd ${projectDir} + - ${ + packageManager === 'yarn' ? 'yarn' : 'npm run' + } dev or follow directions in ${createTerminalLink( + 'README.md', + `file://${path.resolve(projectDir, 'README.md')}`, + )} + + ${header('Documentation:')} + + - ${createTerminalLink( + 'Getting Started', + 'https://payloadcms.com/docs/getting-started/what-is-payload', + )} + - ${createTerminalLink('Configuration', 'https://payloadcms.com/docs/configuration/overview')} + +` +} + +// Create terminalLink with fallback for unsupported terminals +function createTerminalLink(text: string, url: string) { + return terminalLink(text, url, { + fallback: (text, url) => `${text}: ${url}`, + }) +} diff --git a/packages/create-payload-app/tsconfig.json b/packages/create-payload-app/tsconfig.json new file mode 100644 index 000000000..7da6b51b7 --- /dev/null +++ b/packages/create-payload-app/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, // Make sure typescript knows that this module depends on their references + "noEmit": false /* Do not emit outputs. */, + "emitDeclarationOnly": true, + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + "rootDir": "./src" /* Specify the root folder within your source files. */ + }, + "exclude": [ + "dist", + "build", + "tests", + "test", + "node_modules", + ".eslintrc.js", + "src/**/*.spec.js", + "src/**/*.spec.jsx", + "src/**/*.spec.ts", + "src/**/*.spec.tsx" + ], + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"], + "references": [{ "path": "../payload" }] +} diff --git a/packages/db-postgres/package.json b/packages/db-postgres/package.json index 0b3fdbaab..86d29b119 100644 --- a/packages/db-postgres/package.json +++ b/packages/db-postgres/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/db-postgres", - "version": "0.1.3", + "version": "0.1.4", "description": "The officially supported Postgres database adapter for Payload", "repository": "https://github.com/payloadcms/payload", "license": "MIT", diff --git a/packages/db-postgres/src/find/findMany.ts b/packages/db-postgres/src/find/findMany.ts index 05847ab97..478b231dd 100644 --- a/packages/db-postgres/src/find/findMany.ts +++ b/packages/db-postgres/src/find/findMany.ts @@ -150,7 +150,10 @@ export const findMany = async function find({ const countResult = await chainMethods({ methods: selectCountMethods, query: db - .select({ count: sql`count(*)` }) + .select({ + count: sql`count + (*)`, + }) .from(table) .where(where), }) diff --git a/packages/db-postgres/src/queries/parseParams.ts b/packages/db-postgres/src/queries/parseParams.ts index 715461f1c..ba95dafde 100644 --- a/packages/db-postgres/src/queries/parseParams.ts +++ b/packages/db-postgres/src/queries/parseParams.ts @@ -2,7 +2,7 @@ import type { SQL } from 'drizzle-orm' import type { Field, Operator, Where } from 'payload/types' -import { and, ilike, isNotNull, isNull, ne, or, sql } from 'drizzle-orm' +import { and, ilike, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm' import { QueryError } from 'payload/errors' import { validOperators } from 'payload/types' @@ -147,6 +147,7 @@ export async function parseParams({ const { operator: queryOperator, value: queryValue } = sanitizeQueryValue({ field, operator, + relationOrPath, val, }) @@ -158,6 +159,17 @@ export async function parseParams({ ne(rawColumn || table[columnName], queryValue), ), ) + } else if ( + (field.type === 'relationship' || field.type === 'upload') && + Array.isArray(queryValue) && + operator === 'not_in' + ) { + constraints.push( + sql`${notInArray(table[columnName], queryValue)} OR + ${table[columnName]} + IS + NULL`, + ) } else { constraints.push( operatorMap[queryOperator](rawColumn || table[columnName], queryValue), diff --git a/packages/db-postgres/src/queries/sanitizeQueryValue.ts b/packages/db-postgres/src/queries/sanitizeQueryValue.ts index 148318e75..f041f3d0b 100644 --- a/packages/db-postgres/src/queries/sanitizeQueryValue.ts +++ b/packages/db-postgres/src/queries/sanitizeQueryValue.ts @@ -5,12 +5,14 @@ import { createArrayFromCommaDelineated } from 'payload/utilities' type SanitizeQueryValueArgs = { field: Field | TabAsField operator: string + relationOrPath: string val: any } export const sanitizeQueryValue = ({ field, operator: operatorArg, + relationOrPath, val, }: SanitizeQueryValueArgs): { operator: string; value: unknown } => { let operator = operatorArg @@ -18,6 +20,22 @@ export const sanitizeQueryValue = ({ if (!fieldAffectsData(field)) return { operator, value: formattedValue } + if ( + (field.type === 'relationship' || field.type === 'upload') && + !relationOrPath.endsWith('relationTo') && + Array.isArray(formattedValue) + ) { + const allPossibleIDTypes: (number | string)[] = [] + formattedValue.forEach((val) => { + if (typeof val === 'string') { + allPossibleIDTypes.push(val, parseInt(val)) + } else { + allPossibleIDTypes.push(val, String(val)) + } + }) + formattedValue = allPossibleIDTypes + } + // Cast incoming values as proper searchable types if (field.type === 'checkbox' && typeof val === 'string') { if (val.toLowerCase() === 'true') formattedValue = true diff --git a/packages/live-preview-react/package.json b/packages/live-preview-react/package.json index 246cf902b..4113de421 100644 --- a/packages/live-preview-react/package.json +++ b/packages/live-preview-react/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/live-preview-react", - "version": "0.1.2", + "version": "0.1.3", "description": "The official live preview React SDK for Payload", "repository": "https://github.com/payloadcms/payload", "license": "MIT", diff --git a/packages/live-preview/package.json b/packages/live-preview/package.json index cc9087d5c..742bdd5ca 100644 --- a/packages/live-preview/package.json +++ b/packages/live-preview/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/live-preview", - "version": "0.1.2", + "version": "0.1.3", "description": "The official live preview JavaScript SDK for Payload", "repository": "https://github.com/payloadcms/payload", "license": "MIT", diff --git a/packages/payload/package.json b/packages/payload/package.json index edbbf8e8b..68b4a64f9 100644 --- a/packages/payload/package.json +++ b/packages/payload/package.json @@ -1,6 +1,6 @@ { "name": "payload", - "version": "2.0.4", + "version": "2.0.5", "description": "Node, React and MongoDB Headless CMS and Application Framework", "license": "MIT", "main": "./dist/index.js", @@ -208,8 +208,7 @@ "webpack": "^5.78.0" }, "engines": { - "node": ">=14", - "pnpm": ">=8" + "node": ">=14" }, "files": [ "bin.js", diff --git a/packages/payload/src/admin/components/elements/DocumentControls/index.tsx b/packages/payload/src/admin/components/elements/DocumentControls/index.tsx index 9ba2e6513..be034ad2d 100644 --- a/packages/payload/src/admin/components/elements/DocumentControls/index.tsx +++ b/packages/payload/src/admin/components/elements/DocumentControls/index.tsx @@ -55,24 +55,6 @@ export const DocumentControls: React.FC<{ const { i18n, t } = useTranslation('general') - let showPreviewButton = false - - if (collection) { - showPreviewButton = - isEditing && - collection?.admin?.preview && - collection?.versions?.drafts && - !collection?.versions?.drafts?.autosave - } - - if (global) { - showPreviewButton = - isEditing && - global?.admin?.preview && - global?.versions?.drafts && - !global?.versions?.drafts?.autosave - } - const showDotMenu = Boolean(collection && id && !disableActions) return ( @@ -165,9 +147,12 @@ export const DocumentControls: React.FC<{
- {showPreviewButton && ( + {(collection?.admin?.preview || global?.admin?.preview) && ( )} @@ -178,13 +163,26 @@ export const DocumentControls: React.FC<{ {((collection?.versions?.drafts && !collection?.versions?.drafts?.autosave) || (global?.versions?.drafts && !global?.versions?.drafts?.autosave)) && ( )} - + ) : ( - + )} )} diff --git a/packages/payload/src/admin/components/elements/DuplicateDocument/index.tsx b/packages/payload/src/admin/components/elements/DuplicateDocument/index.tsx index 0a888019c..36cf9ab9f 100644 --- a/packages/payload/src/admin/components/elements/DuplicateDocument/index.tsx +++ b/packages/payload/src/admin/components/elements/DuplicateDocument/index.tsx @@ -51,6 +51,7 @@ const Duplicate: React.FC = ({ id, collection, slug }) => { }, params: { depth: 0, + draft: true, locale, }, }) diff --git a/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/addFieldStatePromise.ts b/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/addFieldStatePromise.ts index 3615de734..3e6217e1b 100644 --- a/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/addFieldStatePromise.ts +++ b/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/addFieldStatePromise.ts @@ -61,6 +61,7 @@ export const addFieldStatePromise = async ({ user, value: data?.[field.name], }) + if (data?.[field.name]) { data[field.name] = valueWithDefault } @@ -145,8 +146,8 @@ export const addFieldStatePromise = async ({ fieldState.value = null fieldState.initialValue = null } else { - fieldState.value = arrayValue - fieldState.initialValue = arrayValue + fieldState.value = arrayValue.length + fieldState.initialValue = arrayValue.length if (arrayValue.length > 0) { fieldState.disableFormData = true @@ -236,8 +237,8 @@ export const addFieldStatePromise = async ({ fieldState.value = null fieldState.initialValue = null } else { - fieldState.value = blocksValue - fieldState.initialValue = blocksValue + fieldState.value = blocksValue.length + fieldState.initialValue = blocksValue.length if (blocksValue.length > 0) { fieldState.disableFormData = true diff --git a/packages/payload/src/admin/components/forms/Form/fieldReducer.ts b/packages/payload/src/admin/components/forms/Form/fieldReducer.ts index 4317dcf6a..a2a665f27 100644 --- a/packages/payload/src/admin/components/forms/Form/fieldReducer.ts +++ b/packages/payload/src/admin/components/forms/Form/fieldReducer.ts @@ -8,6 +8,9 @@ import getSiblingData from './getSiblingData' import reduceFieldsToValues from './reduceFieldsToValues' import { flattenRows, separateRows } from './rows' +/** + * Reducer which modifies the form field state (all the current data of the fields in the form). When called using dispatch, it will return a new state object. + */ export function fieldReducer(state: Fields, action: FieldAction): Fields { switch (action.type) { case 'REPLACE_STATE': { @@ -123,7 +126,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields { ...state[path], disableFormData: rows.length > 0, rows: rowsMetadata, - value: rows, + value: rows.length, }, ...flattenRows(path, rows), } @@ -161,10 +164,6 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields { const { remainingFields, rows: siblingRows } = separateRows(path, state) siblingRows.splice(rowIndex, 0, subFieldState) - // add new row to array _value_ - const currentValue = (Array.isArray(state[path]?.value) ? state[path]?.value : []) as Fields[] - const newValue = currentValue.splice(rowIndex, 0, reduceFieldsToValues(subFieldState, true)) - const newState: Fields = { ...remainingFields, ...flattenRows(path, siblingRows), @@ -172,7 +171,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields { ...state[path], disableFormData: true, rows: rowsMetadata, - value: newValue, + value: siblingRows.length, }, } @@ -203,10 +202,6 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields { // replace form _field state_ siblingRows[rowIndex] = subFieldState - // replace array _value_ - const newValue = Array.isArray(state[path]?.value) ? state[path]?.value : [] - newValue[rowIndex] = reduceFieldsToValues(subFieldState, true) - const newState: Fields = { ...remainingFields, ...flattenRows(path, siblingRows), @@ -214,7 +209,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields { ...state[path], disableFormData: true, rows: rowsMetadata, - value: newValue, + value: siblingRows.length, }, } @@ -245,7 +240,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields { ...state[path], disableFormData: true, rows: rowsMetadata, - value: rows, + value: rows.length, }, ...flattenRows(path, rows), } diff --git a/packages/payload/src/admin/components/forms/Form/index.tsx b/packages/payload/src/admin/components/forms/Form/index.tsx index 30effad5c..344164de9 100644 --- a/packages/payload/src/admin/components/forms/Form/index.tsx +++ b/packages/payload/src/admin/components/forms/Form/index.tsx @@ -50,12 +50,15 @@ import reduceFieldsToValues from './reduceFieldsToValues' const baseClass = 'form' const Form: React.FC = (props) => { + const { id, collection, getDocPreferences, global } = useDocumentInfo() + const { action, children, className, disableSuccessStatus, disabled, + fields: fieldsFromProps = collection?.fields || global?.fields, handleResponse, initialData, // values only, paths are required as key - form should build initial state as convenience initialState, // fully formed initial field state @@ -71,7 +74,6 @@ const Form: React.FC = (props) => { const { code: locale } = useLocale() const { i18n, t } = useTranslation('general') const { refreshCookie, user } = useAuth() - const { id, collection, getDocPreferences, global } = useDocumentInfo() const operation = useOperation() const config = useConfig() @@ -90,6 +92,10 @@ const Form: React.FC = (props) => { if (initialState) initialFieldState = initialState const fieldsReducer = useReducer(fieldReducer, {}, () => initialFieldState) + /** + * `fields` is the current, up-to-date state/data of all fields in the form. It can be modified by using dispatchFields, + * which calls the fieldReducer, which then updates the state. + */ const [fields, dispatchFields] = fieldsReducer contextRef.current.fields = fields @@ -167,7 +173,13 @@ const Form: React.FC = (props) => { let validationResult: boolean | string = true if (typeof field.validate === 'function') { - validationResult = await field.validate(field.value, { + let valueToValidate = field.value + + if (field?.rows && Array.isArray(field.rows)) { + valueToValidate = contextRef.current.getDataByPath(path) + } + + validationResult = await field.validate(valueToValidate, { id, config, data, @@ -434,7 +446,7 @@ const Form: React.FC = (props) => { const getRowSchemaByPath = React.useCallback( ({ blockType, path }: { blockType?: string; path: string }) => { const rowConfig = traverseRowConfigs({ - fieldConfig: collection?.fields || global?.fields, + fieldConfig: fieldsFromProps, path, }) const rowFieldConfigs = buildFieldSchemaMap(rowConfig) @@ -442,10 +454,11 @@ const Form: React.FC = (props) => { const fieldKey = pathSegments.at(-1) return rowFieldConfigs.get(blockType ? `${fieldKey}.${blockType}` : fieldKey) }, - [traverseRowConfigs, collection?.fields, global?.fields], + [traverseRowConfigs, fieldsFromProps], ) - // Array/Block row manipulation + // Array/Block row manipulation. This is called when, for example, you add a new block to a blocks field. + // The block data is saved in the rows property of the state, which is modified updated here. const addFieldRow: Context['addFieldRow'] = useCallback( async ({ data, path, rowIndex }) => { const preferences = await getDocPreferences() diff --git a/packages/payload/src/admin/components/forms/Form/types.ts b/packages/payload/src/admin/components/forms/Form/types.ts index 242b0c4c8..28bfcaade 100644 --- a/packages/payload/src/admin/components/forms/Form/types.ts +++ b/packages/payload/src/admin/components/forms/Form/types.ts @@ -2,7 +2,12 @@ import type React from 'react' import type { Dispatch } from 'react' import type { User } from '../../../../auth/types' -import type { Condition, Field as FieldConfig, Validate } from '../../../../fields/config/types' +import type { + Condition, + Field, + Field as FieldConfig, + Validate, +} from '../../../../fields/config/types' export type Row = { blockType?: string @@ -41,6 +46,12 @@ export type Props = { className?: string disableSuccessStatus?: boolean disabled?: boolean + /** + * By default, the form will get the field schema (not data) from the current document. If you pass this in, you can override that behavior. + * This is very useful for sub-forms, where the form's field schema is not necessarily the field schema of the current document (e.g. for the Blocks + * feature of the Lexical Rich Text field) + */ + fields?: Field[] handleResponse?: (res: Response) => void initialData?: Data initialState?: Fields diff --git a/packages/payload/src/admin/components/forms/RenderFields/index.tsx b/packages/payload/src/admin/components/forms/RenderFields/index.tsx index f99d064aa..e300c446f 100644 --- a/packages/payload/src/admin/components/forms/RenderFields/index.tsx +++ b/packages/payload/src/admin/components/forms/RenderFields/index.tsx @@ -17,10 +17,21 @@ const intersectionObserverOptions = { rootMargin: '1000px', } -// If you send `fields` through, it will render those fields explicitly -// Otherwise, it will reduce your fields using the other provided props -// This is so that we can conditionally render fields before reducing them, if desired -// See the sidebar in '../collections/Edit/Default/index.tsx' for an example +/** + * If you send `fields` through, it will render those fields explicitly + * Otherwise, it will reduce your fields using the other provided props + * This is so that we can conditionally render fields before reducing them, if desired + * See the sidebar in '../collections/Edit/Default/index.tsx' for an example + * + * The state/data for the fields it renders is not managed by this component. Instead, every component it renders has + * their own handling of their own value, usually through the useField hook. This hook will get the field's value + * from the Form the field is in, using the field's path. + * + * Thus, if you would like to set the value of a field you render here, you must do so in the Form that contains the field, or in the + * Field component itself. + * + * All this component does is render the field's Field Components, and pass them the props they need to function. + **/ const RenderFields: React.FC = (props) => { const { className, fieldTypes, forceRender, margins } = props diff --git a/packages/payload/src/admin/components/forms/RenderFields/types.ts b/packages/payload/src/admin/components/forms/RenderFields/types.ts index 027a3c4bd..cf44d1438 100644 --- a/packages/payload/src/admin/components/forms/RenderFields/types.ts +++ b/packages/payload/src/admin/components/forms/RenderFields/types.ts @@ -6,21 +6,23 @@ import type { ReducedField } from './filterFields' export type Props = { className?: string fieldTypes: FieldTypes - margins?: 'small' | false forceRender?: boolean + margins?: 'small' | false + permissions?: + | { + [field: string]: FieldPermissions + } + | FieldPermissions + readOnly?: boolean } & ( | { + // Fields to be filtered by the component fieldSchema: FieldWithPath[] filter?: (field: Field) => boolean indexPath?: string - permissions?: - | { - [field: string]: FieldPermissions - } - | FieldPermissions - readOnly?: boolean } | { + // Pre-filtered fields to be simply rendered fields: ReducedField[] } ) diff --git a/packages/payload/src/admin/components/forms/field-types/Array/index.tsx b/packages/payload/src/admin/components/forms/field-types/Array/index.tsx index 4a29a03c9..8884b1d04 100644 --- a/packages/payload/src/admin/components/forms/field-types/Array/index.tsx +++ b/packages/payload/src/admin/components/forms/field-types/Array/index.tsx @@ -91,7 +91,7 @@ const ArrayFieldType: React.FC = (props) => { showError, valid, value, - } = useField<[]>({ + } = useField({ condition, hasRows: true, path, @@ -123,8 +123,8 @@ const ArrayFieldType: React.FC = (props) => { ) const removeRow = useCallback( - async (rowIndex: number) => { - await removeFieldRow({ path, rowIndex }) + (rowIndex: number) => { + removeFieldRow({ path, rowIndex }) setModified(true) }, [removeFieldRow, path, setModified], @@ -278,7 +278,7 @@ const ArrayFieldType: React.FC = (props) => { icon="plus" iconPosition="left" iconStyle="with-border" - onClick={() => addRow(value?.length || 0)} + onClick={() => addRow(value || 0)} > {t('addLabel', { label: getTranslation(labels.singular, i18n) })} diff --git a/packages/payload/src/admin/components/forms/field-types/Blocks/index.tsx b/packages/payload/src/admin/components/forms/field-types/Blocks/index.tsx index b30b3529b..df285cdea 100644 --- a/packages/payload/src/admin/components/forms/field-types/Blocks/index.tsx +++ b/packages/payload/src/admin/components/forms/field-types/Blocks/index.tsx @@ -90,7 +90,7 @@ const BlocksField: React.FC = (props) => { showError, valid, value, - } = useField<[]>({ + } = useField({ condition, hasRows: true, path, @@ -128,8 +128,8 @@ const BlocksField: React.FC = (props) => { ) const removeRow = useCallback( - async (rowIndex: number) => { - await removeFieldRow({ path, rowIndex }) + (rowIndex: number) => { + removeFieldRow({ path, rowIndex }) setModified(true) }, [path, removeFieldRow, setModified], @@ -297,7 +297,7 @@ const BlocksField: React.FC = (props) => { = Omit< - RichTextField, +export type RichTextFieldProps = Omit< + RichTextField, 'type' > & { path?: string } -export type RichTextAdapter = { - CellComponent: React.FC>> - FieldComponent: React.FC> +export type RichTextAdapter = { + CellComponent: React.FC>> + FieldComponent: React.FC> afterReadPromise?: (data: { currentDepth?: number depth: number - field: RichTextField + field: RichTextField overrideAccess?: boolean req: PayloadRequest showHiddenFields: boolean siblingDoc: Record }) => Promise | null - validate: Validate> + validate: Validate> } diff --git a/packages/payload/src/admin/components/forms/useField/index.tsx b/packages/payload/src/admin/components/forms/useField/index.tsx index 7955e9e45..32232bc0a 100644 --- a/packages/payload/src/admin/components/forms/useField/index.tsx +++ b/packages/payload/src/admin/components/forms/useField/index.tsx @@ -29,7 +29,7 @@ const useField = (options: Options): FieldType => { const dispatchField = useFormFields(([_, dispatch]) => dispatch) const config = useConfig() - const { getData, getSiblingData, setModified } = useForm() + const { getData, getDataByPath, getSiblingData, setModified } = useForm() const value = field?.value as T const initialValue = field?.initialValue as T @@ -116,8 +116,14 @@ const useField = (options: Options): FieldType => { user, } + let valueToValidate = value + + if (field?.rows && Array.isArray(field.rows)) { + valueToValidate = getDataByPath(path) + } + const validationResult = - typeof validate === 'function' ? await validate(value, validateOptions) : true + typeof validate === 'function' ? await validate(valueToValidate, validateOptions) : true if (typeof validationResult === 'string') { action.errorMessage = validationResult @@ -132,7 +138,7 @@ const useField = (options: Options): FieldType => { } } - validateField() + void validateField() }, 150, [ @@ -142,6 +148,7 @@ const useField = (options: Options): FieldType => { dispatchField, getData, getSiblingData, + getDataByPath, id, operation, path, diff --git a/packages/payload/src/admin/components/views/Global/Default/index.scss b/packages/payload/src/admin/components/views/Global/Default/index.scss index ab49b1711..09ab7239e 100644 --- a/packages/payload/src/admin/components/views/Global/Default/index.scss +++ b/packages/payload/src/admin/components/views/Global/Default/index.scss @@ -27,7 +27,8 @@ } &__fields { - & > .tabs-field { + & > .tabs-field, + & > .group-field { margin-right: calc(var(--base) * -2); } } @@ -51,7 +52,7 @@ position: sticky; top: var(--doc-controls-height); width: 33.33%; - height: 100%; + height: calc(100vh - var(--doc-controls-height)); } &__sidebar { @@ -110,7 +111,8 @@ } &__fields { - & > .tabs-field { + & > .tabs-field, + & > .group-field { margin-right: calc(var(--gutter-h) * -1); } } diff --git a/packages/payload/src/admin/components/views/Global/Default/index.tsx b/packages/payload/src/admin/components/views/Global/Default/index.tsx index 9664cde9c..ad2f503e5 100644 --- a/packages/payload/src/admin/components/views/Global/Default/index.tsx +++ b/packages/payload/src/admin/components/views/Global/Default/index.tsx @@ -90,9 +90,8 @@ export const DefaultGlobalEdit: React.FC = (props) => {
field.admin.position === 'sidebar'} + fields={sidebarFields} permissions={permissions.fields} readOnly={!hasSavePermission} /> diff --git a/packages/payload/src/admin/components/views/LivePreview/Context/context.ts b/packages/payload/src/admin/components/views/LivePreview/Context/context.ts index 0c6f2a4c4..e3ecd9f90 100644 --- a/packages/payload/src/admin/components/views/LivePreview/Context/context.ts +++ b/packages/payload/src/admin/components/views/LivePreview/Context/context.ts @@ -8,7 +8,6 @@ import type { SizeReducerAction } from './sizeReducer' export interface LivePreviewContextType { breakpoint: LivePreviewConfig['breakpoints'][number]['name'] breakpoints: LivePreviewConfig['breakpoints'] - deviceFrameRef: React.RefObject iframeHasLoaded: boolean iframeRef: React.RefObject measuredDeviceSize: { @@ -18,6 +17,7 @@ export interface LivePreviewContextType { setBreakpoint: (breakpoint: LivePreviewConfig['breakpoints'][number]['name']) => void setHeight: (height: number) => void setIframeHasLoaded: (loaded: boolean) => void + setMeasuredDeviceSize: (size: { height: number; width: number }) => void setSize: Dispatch setToolbarPosition: (position: { x: number; y: number }) => void setWidth: (width: number) => void @@ -36,7 +36,6 @@ export interface LivePreviewContextType { export const LivePreviewContext = createContext({ breakpoint: undefined, breakpoints: undefined, - deviceFrameRef: undefined, iframeHasLoaded: false, iframeRef: undefined, measuredDeviceSize: { @@ -46,6 +45,7 @@ export const LivePreviewContext = createContext({ setBreakpoint: () => {}, setHeight: () => {}, setIframeHasLoaded: () => {}, + setMeasuredDeviceSize: () => {}, setSize: () => {}, setToolbarPosition: () => {}, setWidth: () => {}, diff --git a/packages/payload/src/admin/components/views/LivePreview/Context/index.tsx b/packages/payload/src/admin/components/views/LivePreview/Context/index.tsx index ce9806170..b268e1014 100644 --- a/packages/payload/src/admin/components/views/LivePreview/Context/index.tsx +++ b/packages/payload/src/admin/components/views/LivePreview/Context/index.tsx @@ -5,7 +5,6 @@ import type { LivePreviewConfig } from '../../../../../exports/config' import type { EditViewProps } from '../../types' import type { usePopupWindow } from '../usePopupWindow' -import { useResize } from '../../../../utilities/useResize' import { customCollisionDetection } from './collisionDetection' import { LivePreviewContext } from './context' import { sizeReducer } from './sizeReducer' @@ -26,8 +25,6 @@ export const LivePreviewProvider: React.FC = (props) => { const iframeRef = React.useRef(null) - const deviceFrameRef = React.useRef(null) - const [iframeHasLoaded, setIframeHasLoaded] = React.useState(false) const [zoom, setZoom] = React.useState(1) @@ -36,6 +33,11 @@ export const LivePreviewProvider: React.FC = (props) => { const [size, setSize] = React.useReducer(sizeReducer, { height: 0, width: 0 }) + const [measuredDeviceSize, setMeasuredDeviceSize] = React.useState({ + height: 0, + width: 0, + }) + const [breakpoint, setBreakpoint] = React.useState('responsive') @@ -92,22 +94,18 @@ export const LivePreviewProvider: React.FC = (props) => { } }, [breakpoint, breakpoints]) - // keep an accurate measurement of the actual device size as it is truly rendered - // this is helpful when `sizes` are non-number units like percentages, etc. - const { size: measuredDeviceSize } = useResize(deviceFrameRef) - return ( = (props) => { const { children } = props - const { breakpoint, deviceFrameRef, size, zoom } = useLivePreviewContext() + const deviceFrameRef = React.useRef(null) + + const { breakpoint, setMeasuredDeviceSize, size, zoom } = useLivePreviewContext() + + // Keep an accurate measurement of the actual device size as it is truly rendered + // This is helpful when `sizes` are non-number units like percentages, etc. + const { size: measuredDeviceSize } = useResize(deviceFrameRef) + + // Sync the measured device size with the context so that other components can use it + // This happens from the bottom up so that as this component mounts and unmounts, + // Its size is freshly populated again upon re-mounting, i.e. going from iframe->popup->iframe + useEffect(() => { + if (measuredDeviceSize) { + setMeasuredDeviceSize(measuredDeviceSize) + } + }, [measuredDeviceSize, setMeasuredDeviceSize]) let x = '0' let margin = '0' diff --git a/packages/payload/src/admin/components/views/LivePreview/DeviceContainer/index.tsx b/packages/payload/src/admin/components/views/LivePreview/DeviceContainer/index.tsx index dc00e6d9c..4b62e04e3 100644 --- a/packages/payload/src/admin/components/views/LivePreview/DeviceContainer/index.tsx +++ b/packages/payload/src/admin/components/views/LivePreview/DeviceContainer/index.tsx @@ -7,7 +7,7 @@ export const DeviceContainer: React.FC<{ }> = (props) => { const { children } = props - const { breakpoint, breakpoints, deviceFrameRef, size, zoom } = useLivePreviewContext() + const { breakpoint, breakpoints, size, zoom } = useLivePreviewContext() const foundBreakpoint = breakpoint && breakpoints?.find((bp) => bp.name === breakpoint) @@ -31,7 +31,6 @@ export const DeviceContainer: React.FC<{ return (
{ } } + const prefillForm = autoLogin && autoLogin.prefillOnly + return ( {user ? ( @@ -75,22 +77,33 @@ const Login: React.FC = () => { action={`${serverURL}${api}/${userSlug}/login`} className={`${baseClass}__form`} disableSuccessStatus - initialData={{ - email: autoLogin && autoLogin.prefillOnly ? autoLogin.email : undefined, - password: autoLogin && autoLogin.prefillOnly ? autoLogin.password : undefined, - }} + initialData={ + prefillForm + ? { + email: autoLogin.email, + password: autoLogin.password, + } + : undefined + } method="post" onSuccess={onSuccess} waitForAutocomplete > - - +
+ + +
{t('forgotPasswordQuestion')} {t('login')} diff --git a/packages/payload/src/admin/components/views/collections/Edit/Default/index.scss b/packages/payload/src/admin/components/views/collections/Edit/Default/index.scss index 117953590..8db97d444 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/Default/index.scss +++ b/packages/payload/src/admin/components/views/collections/Edit/Default/index.scss @@ -27,7 +27,8 @@ } &__fields { - & > .tabs-field { + & > .tabs-field, + & > .group-field { margin-right: calc(var(--base) * -2); } } @@ -55,7 +56,7 @@ position: sticky; top: var(--doc-controls-height); width: 33.33%; - height: 100%; + height: calc(100vh - var(--doc-controls-height)); } &__sidebar { @@ -106,7 +107,8 @@ } &__fields { - & > .tabs-field { + & > .tabs-field, + & > .group-field { margin-right: calc(var(--gutter-h) * -1); } } diff --git a/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx b/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx index 22b675410..1b7f8ff54 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx @@ -115,7 +115,12 @@ export const DefaultCollectionEdit: React.FC = (props)
- +
diff --git a/packages/payload/src/collections/operations/find.ts b/packages/payload/src/collections/operations/find.ts index 0566cd980..23eb0e3ca 100644 --- a/packages/payload/src/collections/operations/find.ts +++ b/packages/payload/src/collections/operations/find.ts @@ -12,6 +12,7 @@ import { initTransaction } from '../../utilities/initTransaction' import { killTransaction } from '../../utilities/killTransaction' import { buildVersionCollectionFields } from '../../versions/buildCollectionFields' import { appendVersionToQueryKey } from '../../versions/drafts/appendVersionToQueryKey' +import { getQueryDraftsSort } from '../../versions/drafts/getQueryDraftsSort' import { buildAfterOperation } from './utils' export type Arguments = { @@ -127,7 +128,7 @@ async function find>( page: sanitizedPage, pagination: usePagination, req, - sort, + sort: getQueryDraftsSort(sort), where: fullWhere, }) } else { diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index 106451dc2..65003877f 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -517,7 +517,7 @@ export type Config = { */ defaultMaxTextLength?: number /** Default richtext editor to use for richText fields */ - editor: RichTextAdapter + editor: RichTextAdapter /** * Email configuration options. This value is overridden by `email` in Payload.init if passed. * diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index 120464d75..b75923873 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -398,11 +398,13 @@ export type RelationshipValue = | ValueWithRelation[] | (number | string) -export type RichTextField = FieldBase & { +type IsAny = 0 extends 1 & T ? true : false + +export type RichTextField = FieldBase & { admin?: Admin - editor?: RichTextAdapter + editor?: RichTextAdapter type: 'richText' -} & AdapterProps +} & (IsAny extends true ? {} : AdapterProps) export type ArrayField = FieldBase & { admin?: Admin & { diff --git a/packages/payload/src/fields/validations.ts b/packages/payload/src/fields/validations.ts index 3b797f523..c885cf08f 100644 --- a/packages/payload/src/fields/validations.ts +++ b/packages/payload/src/fields/validations.ts @@ -211,7 +211,7 @@ export const date: Validate = (value, { required, t return true } -export const richText: Validate = async ( +export const richText: Validate = async ( value, options, ) => { @@ -382,7 +382,7 @@ export const relationship: Validate = async }) if (invalidRelationships.length > 0) { - return `This field has the following invalid selections: ${invalidRelationships + return `This relationship field has the following invalid relationships: ${invalidRelationships .map((err, invalid) => { return `${err} ${JSON.stringify(invalid)}` }) diff --git a/packages/payload/src/translations/bg.json b/packages/payload/src/translations/bg.json index 07689cc19..28dfe863f 100644 --- a/packages/payload/src/translations/bg.json +++ b/packages/payload/src/translations/bg.json @@ -146,7 +146,7 @@ "addFilter": "Добави филтър", "adminTheme": "Цветова тема", "and": "И", - "applyChanges": "Приложете промените", + "applyChanges": "Приложи промените", "ascending": "Възходящ", "automatic": "Автоматична", "backToDashboard": "Обратно към таблото", @@ -176,7 +176,7 @@ "deletedSuccessfully": "Изтрито успешно.", "deleting": "Изтриване...", "descending": "Низходящо", - "deselectAllRows": "Деселектирайте всички редове", + "deselectAllRows": "Деселектирай всички редове", "duplicate": "Дупликирай", "duplicateWithoutSaving": "Дупликирай без да запазваш промените", "edit": "Редактирай", @@ -231,7 +231,7 @@ "saving": "Запазване...", "searchBy": "Търси по {{label}}", "selectAll": "Избери всички {{count}} {{label}}", - "selectAllRows": "Изберете всички редове", + "selectAllRows": "Избери всички редове", "selectValue": "Избери стойност", "selectedCount": "{{count}} {{label}} избрани", "showAllLabel": "Покажи всички {{label}}", @@ -273,23 +273,23 @@ "near": "близко" }, "upload": { - "crop": "Реколта", - "cropToolDescription": "Плъзнете ъглите на избраната област, нарисувайте нова област или коригирайте стойностите по-долу.", + "crop": "Изрязване", + "cropToolDescription": "Плъзни ъглите на избраната област, избери нова област или коригирай стойностите по-долу.", "dragAndDrop": "Дръпни и пусни файл", "dragAndDropHere": "или дръпни и пусни файла тук", "editImage": "Редактирай изображение", "fileName": "Име на файла", "fileSize": "Големина на файла", "focalPoint": "Фокусна точка", - "focalPointDescription": "Преместете фокусната точка директно върху визуализацията или регулирайте стойностите по-долу.", + "focalPointDescription": "Премести фокусната точка директно върху визуализацията или регулирай стойностите по-долу.", "height": "Височина", "lessInfo": "По-малко информация", "moreInfo": "Повече информация", "previewSizes": "Преглед на размери", "selectCollectionToBrowse": "Избери колекция, която да разгледаш", "selectFile": "Избери файл", - "setCropArea": "Задайте област за изрязване", - "setFocalPoint": "Задайте фокусна точка", + "setCropArea": "Задай област за изрязване", + "setFocalPoint": "Задай фокусна точка", "sizes": "Големини", "sizesFor": "Размери за {{label}}", "width": "Ширина" @@ -368,4 +368,4 @@ "viewingVersions": "Гледане на версии за {{entityLabel}} {{documentTitle}}", "viewingVersionsGlobal": "Гледане на версии за глобалния документ {{entityLabel}}" } -} \ No newline at end of file +} diff --git a/packages/payload/src/versions/drafts/getQueryDraftsSort.ts b/packages/payload/src/versions/drafts/getQueryDraftsSort.ts new file mode 100644 index 000000000..3da4ec935 --- /dev/null +++ b/packages/payload/src/versions/drafts/getQueryDraftsSort.ts @@ -0,0 +1,17 @@ +/** + * Takes the incoming sort argument and prefixes it with `versions.` and preserves any `-` prefixes for descending order + * @param sort + */ +export const getQueryDraftsSort = (sort: string): string => { + if (!sort) return sort + + let direction = '' + let orderBy = sort + + if (sort[0] === '-') { + direction = '-' + orderBy = sort.substring(1) + } + + return `${direction}version.${orderBy}` +} diff --git a/packages/richtext-lexical/package.json b/packages/richtext-lexical/package.json index 1f8e572db..31c263f95 100644 --- a/packages/richtext-lexical/package.json +++ b/packages/richtext-lexical/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/richtext-lexical", - "version": "0.1.5", + "version": "0.1.8", "description": "The officially supported Lexical richtext adapter for Payload", "repository": "https://github.com/payloadcms/payload", "license": "MIT", diff --git a/packages/richtext-lexical/src/cell/index.tsx b/packages/richtext-lexical/src/cell/index.tsx index 2289b9c93..f493c047b 100644 --- a/packages/richtext-lexical/src/cell/index.tsx +++ b/packages/richtext-lexical/src/cell/index.tsx @@ -10,22 +10,44 @@ import type { AdapterProps } from '../types' import { getEnabledNodes } from '../field/lexical/nodes' export const RichTextCell: React.FC< - CellComponentProps, SerializedEditorState> & AdapterProps + CellComponentProps, SerializedEditorState> & + AdapterProps > = ({ data, editorConfig }) => { const [preview, setPreview] = React.useState('Loading...') useEffect(() => { - if (data == null) { + let dataToUse = data + if (dataToUse == null) { setPreview('') return } + + // Transform data through load hooks + if (editorConfig?.features?.hooks?.load?.length) { + editorConfig.features.hooks.load.forEach((hook) => { + dataToUse = hook({ incomingEditorState: dataToUse }) + }) + } + + // If data is from Slate and not Lexical + if (dataToUse && Array.isArray(dataToUse) && !('root' in dataToUse)) { + setPreview('') + return + } + + // If data is from payload-plugin-lexical + if (dataToUse && 'jsonContent' in dataToUse) { + setPreview('') + return + } + // initialize headless editor const headlessEditor = createHeadlessEditor({ namespace: editorConfig.lexical.namespace, nodes: getEnabledNodes({ editorConfig }), theme: editorConfig.lexical.theme, }) - headlessEditor.setEditorState(headlessEditor.parseEditorState(data)) + headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse)) const textContent = headlessEditor.getEditorState().read(() => { diff --git a/packages/richtext-lexical/src/field/Field.tsx b/packages/richtext-lexical/src/field/Field.tsx index 647dcd5b9..1eb7cd6a2 100644 --- a/packages/richtext-lexical/src/field/Field.tsx +++ b/packages/richtext-lexical/src/field/Field.tsx @@ -89,9 +89,16 @@ const RichText: React.FC = (props) => { fieldProps={props} initialState={initialValue} onChange={(editorState, editor, tags) => { - const json = editorState.toJSON() + let serializedEditorState = editorState.toJSON() - setValue(json) + // Transform state through save hooks + if (editorConfig?.features?.hooks?.save?.length) { + editorConfig.features.hooks.save.forEach((hook) => { + serializedEditorState = hook({ incomingEditorState: serializedEditorState }) + }) + } + + setValue(serializedEditorState) }} readOnly={readOnly} setValue={setValue} @@ -109,7 +116,7 @@ function fallbackRender({ error }): JSX.Element { // Call resetErrorBoundary() to reset the error boundary and retry the render. return ( -
+

Something went wrong:

{error.message}
diff --git a/packages/richtext-lexical/src/field/features/Blocks/afterReadPromise.ts b/packages/richtext-lexical/src/field/features/Blocks/afterReadPromise.ts index 74566f762..11858c23a 100644 --- a/packages/richtext-lexical/src/field/features/Blocks/afterReadPromise.ts +++ b/packages/richtext-lexical/src/field/features/Blocks/afterReadPromise.ts @@ -1,3 +1,5 @@ +import type { Block } from 'payload/types' + import { sanitizeFields } from 'payload/config' import type { BlocksFeatureProps } from '.' @@ -20,40 +22,42 @@ export const blockAfterReadPromiseHOC = ( showHiddenFields, siblingDoc, }) => { + const blocks: Block[] = props.blocks + const blockFieldData = node.fields.data + const promises: Promise[] = [] // Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here const payloadConfig = req.payload.config const validRelationships = payloadConfig.collections.map((c) => c.slug) || [] - props.blocks = props.blocks.map((block) => { - const unsanitizedBlock = { ...block } - unsanitizedBlock.fields = sanitizeFields({ + blocks.forEach((block) => { + block.fields = sanitizeFields({ config: payloadConfig, fields: block.fields, validRelationships, }) - return unsanitizedBlock }) - if (Array.isArray(props.blocks)) { - props.blocks.forEach((block) => { - if (block?.fields) { - recurseNestedFields({ - afterReadPromises, - currentDepth, - data: node.fields.data || {}, - depth, - fields: block.fields, - overrideAccess, - promises, - req, - showHiddenFields, - siblingDoc, - }) - } - }) + // find block used in this node + const block = props.blocks.find((block) => block.slug === blockFieldData.blockType) + if (!block || !block?.fields?.length || !blockFieldData) { + return promises } + recurseNestedFields({ + afterReadPromises, + currentDepth, + data: blockFieldData, + depth, + fields: block.fields, + overrideAccess, + promises, + req, + showHiddenFields, + // The afterReadPromise gets its data from looking for field.name inside of the siblingDoc. Thus, here we cannot pass the whole document's siblingDoc, but only the siblingDoc (sibling fields) of the current field. + siblingDoc: blockFieldData, + }) + return promises } diff --git a/packages/richtext-lexical/src/field/features/Blocks/component/BlockContent.tsx b/packages/richtext-lexical/src/field/features/Blocks/component/BlockContent.tsx index 351a30c8b..dab2bd9cb 100644 --- a/packages/richtext-lexical/src/field/features/Blocks/component/BlockContent.tsx +++ b/packages/richtext-lexical/src/field/features/Blocks/component/BlockContent.tsx @@ -24,6 +24,11 @@ type Props = { nodeKey: string } +/** + * The actual content of the Block. This should be INSIDE a Form component, + * scoped to the block. All format operations in here are thus scoped to the block's form, and + * not the whole document. + */ export const BlockContent: React.FC = (props) => { const { baseClass, block, field, fields, nodeKey } = props const { i18n } = useTranslation() diff --git a/packages/richtext-lexical/src/field/features/Blocks/component/index.tsx b/packages/richtext-lexical/src/field/features/Blocks/component/index.tsx index 5c43ae8c3..08ba9bb4f 100644 --- a/packages/richtext-lexical/src/field/features/Blocks/component/index.tsx +++ b/packages/richtext-lexical/src/field/features/Blocks/component/index.tsx @@ -1,14 +1,20 @@ import { type ElementFormatType } from 'lexical' import { Form, buildInitialState, useFormSubmitted } from 'payload/components/forms' -import React, { useMemo } from 'react' +import React, { useEffect, useMemo } from 'react' import { type BlockFields } from '../nodes/BlocksNode' const baseClass = 'lexical-block' import type { Data } from 'payload/types' -import { useConfig } from 'payload/components/utilities' +import { + buildStateFromSchema, + useConfig, + useDocumentInfo, + useLocale, +} from 'payload/components/utilities' import { sanitizeFields } from 'payload/config' +import { useTranslation } from 'react-i18next' import type { BlocksFeatureProps } from '..' @@ -43,13 +49,49 @@ export const BlockComponent: React.FC = (props) => { validRelationships, }) - const initialDataRef = React.useRef(buildInitialState(fields.data || {})) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once + const initialStateRef = React.useRef(buildInitialState(fields.data || {})) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once + + const config = useConfig() + const { t } = useTranslation('general') + const { code: locale } = useLocale() + const { getDocPreferences } = useDocumentInfo() + + // initialState State + + const [initialState, setInitialState] = React.useState(null) + + useEffect(() => { + async function buildInitialState() { + const preferences = await getDocPreferences() + + const stateFromSchema = await buildStateFromSchema({ + config, + data: fields.data, + fieldSchema: block.fields, + locale, + operation: 'update', + preferences, + t, + }) + + // We have to merge the output of buildInitialState (above this useEffect) with the output of buildStateFromSchema. + // That's because the output of buildInitialState provides important properties necessary for THIS block, + // like blockName, blockType and id, while buildStateFromSchema provides the correct output of this block's data, + // e.g. if this block has a sub-block (like the `rows` property) + setInitialState({ + ...initialStateRef?.current, + ...stateFromSchema, + }) + } + void buildInitialState() + }, [setInitialState, config, block, locale, getDocPreferences, t]) // do not add fields here, it causes an endless loop // Memoized Form JSX const formContent = useMemo(() => { return ( - block && ( -
+ block && + initialState && ( + = (props) => { ) ) - }, [block, field, nodeKey, submitted]) + }, [block, field, nodeKey, submitted, initialState]) return
{formContent}
} diff --git a/packages/richtext-lexical/src/field/features/Blocks/validate.ts b/packages/richtext-lexical/src/field/features/Blocks/validate.ts index 1a2ca54b0..56f3a6801 100644 --- a/packages/richtext-lexical/src/field/features/Blocks/validate.ts +++ b/packages/richtext-lexical/src/field/features/Blocks/validate.ts @@ -15,12 +15,12 @@ export const blockValidationHOC = ( payloadConfig, validation, }) => { - const blockFieldValues = node.fields.data - + const blockFieldData = node.fields.data const blocks: Block[] = props.blocks + // Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here + const validRelationships = payloadConfig.collections.map((c) => c.slug) || [] blocks.forEach((block) => { - const validRelationships = payloadConfig.collections.map((c) => c.slug) || [] block.fields = sanitizeFields({ config: payloadConfig, fields: block.fields, @@ -29,7 +29,7 @@ export const blockValidationHOC = ( }) // find block - const block = props.blocks.find((block) => block.slug === blockFieldValues.blockType) + const block = props.blocks.find((block) => block.slug === blockFieldData.blockType) // validate block if (!block) { diff --git a/packages/richtext-lexical/src/field/features/Link/afterReadPromise.ts b/packages/richtext-lexical/src/field/features/Link/afterReadPromise.ts index 7d662e14e..dd7271a7c 100644 --- a/packages/richtext-lexical/src/field/features/Link/afterReadPromise.ts +++ b/packages/richtext-lexical/src/field/features/Link/afterReadPromise.ts @@ -52,7 +52,7 @@ export const linkAfterReadPromiseHOC = ( promises, req, showHiddenFields, - siblingDoc, + siblingDoc: node.fields || {}, }) } return promises diff --git a/packages/richtext-lexical/src/field/features/Link/plugins/floatingLinkEditor/LinkEditor/index.tsx b/packages/richtext-lexical/src/field/features/Link/plugins/floatingLinkEditor/LinkEditor/index.tsx index d2c5c7773..13103682c 100644 --- a/packages/richtext-lexical/src/field/features/Link/plugins/floatingLinkEditor/LinkEditor/index.tsx +++ b/packages/richtext-lexical/src/field/features/Link/plugins/floatingLinkEditor/LinkEditor/index.tsx @@ -1,5 +1,5 @@ import type { LexicalCommand } from 'lexical' -import type { Fields } from 'payload/types' +import type { Data, Fields } from 'payload/types' import { useModal } from '@faceless-ui/modal' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' @@ -14,7 +14,6 @@ import { createCommand, } from 'lexical' import { formatDrawerSlug } from 'payload/components/elements' -import { reduceFieldsToValues } from 'payload/components/forms' import { buildStateFromSchema, useAuth, @@ -317,17 +316,14 @@ export function LinkEditor({ { + handleModalSubmit={(fields: Fields, data: Data) => { closeModal(drawerSlug) - const data = reduceFieldsToValues(fields, true) - if (data?.fields?.doc?.value) { data.fields.doc.value = { id: data.fields.doc.value, } } - const newLinkPayload: LinkPayload = data as LinkPayload editor.dispatchCommand(TOGGLE_LINK_COMMAND, newLinkPayload) diff --git a/packages/richtext-lexical/src/field/features/Link/plugins/floatingLinkEditor/index.scss b/packages/richtext-lexical/src/field/features/Link/plugins/floatingLinkEditor/index.scss index 3a04f6581..71d74a511 100644 --- a/packages/richtext-lexical/src/field/features/Link/plugins/floatingLinkEditor/index.scss +++ b/packages/richtext-lexical/src/field/features/Link/plugins/floatingLinkEditor/index.scss @@ -16,7 +16,6 @@ html[data-theme='light'] { position: absolute; top: 0; left: 0; - z-index: 10; opacity: 0; border-radius: 6.25px; transition: opacity 0.2s; diff --git a/packages/richtext-lexical/src/field/features/Upload/afterReadPromise.ts b/packages/richtext-lexical/src/field/features/Upload/afterReadPromise.ts index a650b6983..c4178a204 100644 --- a/packages/richtext-lexical/src/field/features/Upload/afterReadPromise.ts +++ b/packages/richtext-lexical/src/field/features/Upload/afterReadPromise.ts @@ -51,7 +51,7 @@ export const uploadAfterReadPromiseHOC = ( promises, req, showHiddenFields, - siblingDoc, + siblingDoc: node.fields || {}, }) } } diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/heading.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/heading.ts new file mode 100644 index 000000000..ac031c688 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/heading.ts @@ -0,0 +1,25 @@ +import type { SerializedHeadingNode } from '@lexical/rich-text' + +import type { SlateNodeConverter } from '../types' + +import { convertSlateNodesToLexical } from '..' + +export const HeadingConverter: SlateNodeConverter = { + converter({ converters, slateNode }) { + return { + children: convertSlateNodesToLexical({ + canContainParagraphs: false, + converters, + parentNodeType: 'heading', + slateNodes: slateNode.children || [], + }), + direction: 'ltr', + format: '', + indent: 0, + tag: slateNode.type as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6', // Slate puts the tag (h1 / h2 / ...) inside of node.type + type: 'heading', + version: 1, + } as const as SerializedHeadingNode + }, + nodeTypes: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/indent.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/indent.ts new file mode 100644 index 000000000..b5e721297 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/indent.ts @@ -0,0 +1,65 @@ +import type { SerializedLexicalNode, SerializedParagraphNode } from 'lexical' + +import type { SlateNodeConverter } from '../types' + +import { convertSlateNodesToLexical } from '..' + +export const IndentConverter: SlateNodeConverter = { + converter({ converters, slateNode }) { + console.log('slateToLexical > IndentConverter > converter', JSON.stringify(slateNode, null, 2)) + const convertChildren = (node: any, indentLevel: number = 0): SerializedLexicalNode => { + if ( + (node?.type && (!node.children || node.type !== 'indent')) || + (!node?.type && node?.text) + ) { + console.log( + 'slateToLexical > IndentConverter > convertChildren > node', + JSON.stringify(node, null, 2), + ) + console.log( + 'slateToLexical > IndentConverter > convertChildren > nodeOutput', + JSON.stringify( + convertSlateNodesToLexical({ + canContainParagraphs: false, + converters, + parentNodeType: 'indent', + slateNodes: [node], + }), + + null, + 2, + ), + ) + + return { + ...convertSlateNodesToLexical({ + canContainParagraphs: false, + converters, + parentNodeType: 'indent', + slateNodes: [node], + })[0], + indent: indentLevel, + } as const as SerializedLexicalNode + } + + const children = node.children.map((child: any) => convertChildren(child, indentLevel + 1)) + console.log('slateToLexical > IndentConverter > children', JSON.stringify(children, null, 2)) + return { + children: children, + direction: 'ltr', + format: '', + indent: indentLevel, + type: 'paragraph', + version: 1, + } as const as SerializedParagraphNode + } + + console.log( + 'slateToLexical > IndentConverter > output', + JSON.stringify(convertChildren(slateNode), null, 2), + ) + + return convertChildren(slateNode) + }, + nodeTypes: ['indent'], +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/link.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/link.ts new file mode 100644 index 000000000..80458d90b --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/link.ts @@ -0,0 +1,29 @@ +import type { SerializedLinkNode } from '../../../../Link/nodes/LinkNode' +import type { SlateNodeConverter } from '../types' + +import { convertSlateNodesToLexical } from '..' + +export const LinkConverter: SlateNodeConverter = { + converter({ converters, slateNode }) { + return { + children: convertSlateNodesToLexical({ + canContainParagraphs: false, + converters, + parentNodeType: 'link', + slateNodes: slateNode.children || [], + }), + direction: 'ltr', + fields: { + doc: slateNode.doc || undefined, + linkType: slateNode.linkType || 'custom', + newTab: slateNode.newTab || false, + url: slateNode.url || undefined, + }, + format: '', + indent: 0, + type: 'link', + version: 1, + } as const as SerializedLinkNode + }, + nodeTypes: ['link'], +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/listItem.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/listItem.ts new file mode 100644 index 000000000..1775803da --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/listItem.ts @@ -0,0 +1,26 @@ +import type { SerializedListItemNode } from '@lexical/list' + +import type { SlateNodeConverter } from '../types' + +import { convertSlateNodesToLexical } from '..' + +export const ListItemConverter: SlateNodeConverter = { + converter({ childIndex, converters, slateNode }) { + return { + checked: undefined, + children: convertSlateNodesToLexical({ + canContainParagraphs: false, + converters, + parentNodeType: 'listitem', + slateNodes: slateNode.children || [], + }), + direction: 'ltr', + format: '', + indent: 0, + type: 'listitem', + value: childIndex + 1, + version: 1, + } as const as SerializedListItemNode + }, + nodeTypes: ['li'], +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/orderedList.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/orderedList.ts new file mode 100644 index 000000000..d3cef2650 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/orderedList.ts @@ -0,0 +1,27 @@ +import type { SerializedListNode } from '@lexical/list' + +import type { SlateNodeConverter } from '../types' + +import { convertSlateNodesToLexical } from '..' + +export const OrderedListConverter: SlateNodeConverter = { + converter({ converters, slateNode }) { + return { + children: convertSlateNodesToLexical({ + canContainParagraphs: false, + converters, + parentNodeType: 'list', + slateNodes: slateNode.children || [], + }), + direction: 'ltr', + format: '', + indent: 0, + listType: 'number', + start: 1, + tag: 'ol', + type: 'list', + version: 1, + } as const as SerializedListNode + }, + nodeTypes: ['ol'], +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/relationship.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/relationship.ts new file mode 100644 index 000000000..d881e0538 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/relationship.ts @@ -0,0 +1,17 @@ +import type { SerializedRelationshipNode } from '../../../../../..' +import type { SlateNodeConverter } from '../types' + +export const RelationshipConverter: SlateNodeConverter = { + converter({ slateNode }) { + return { + format: '', + relationTo: slateNode.relationTo, + type: 'relationship', + value: { + id: slateNode?.value?.id || '', + }, + version: 1, + } as const as SerializedRelationshipNode + }, + nodeTypes: ['relationship'], +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/unknown.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/unknown.ts new file mode 100644 index 000000000..05fa300ee --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/unknown.ts @@ -0,0 +1,27 @@ +import type { SerializedUnknownConvertedNode } from '../../nodes/unknownConvertedNode' +import type { SlateNodeConverter } from '../types' + +import { convertSlateNodesToLexical } from '..' + +export const UnknownConverter: SlateNodeConverter = { + converter({ converters, slateNode }) { + return { + children: convertSlateNodesToLexical({ + canContainParagraphs: false, + converters, + parentNodeType: 'unknownConverted', + slateNodes: slateNode.children || [], + }), + data: { + nodeData: slateNode, + nodeType: slateNode.type, + }, + direction: 'ltr', + format: '', + indent: 0, + type: 'unknownConverted', + version: 1, + } as const as SerializedUnknownConvertedNode + }, + nodeTypes: ['unknown'], +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/unorderedList.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/unorderedList.ts new file mode 100644 index 000000000..fd82b3a7e --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/unorderedList.ts @@ -0,0 +1,27 @@ +import type { SerializedListNode } from '@lexical/list' + +import type { SlateNodeConverter } from '../types' + +import { convertSlateNodesToLexical } from '..' + +export const UnorderedListConverter: SlateNodeConverter = { + converter({ converters, slateNode }) { + return { + children: convertSlateNodesToLexical({ + canContainParagraphs: false, + converters, + parentNodeType: 'list', + slateNodes: slateNode.children || [], + }), + direction: 'ltr', + format: '', + indent: 0, + listType: 'bullet', + start: 1, + tag: 'ul', + type: 'list', + version: 1, + } as const as SerializedListNode + }, + nodeTypes: ['ul'], +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/upload.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/upload.ts new file mode 100644 index 000000000..a3b59d6a5 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/converters/upload.ts @@ -0,0 +1,20 @@ +import type { SerializedUploadNode } from '../../../../../..' +import type { SlateNodeConverter } from '../types' + +export const UploadConverter: SlateNodeConverter = { + converter({ slateNode }) { + return { + fields: { + ...slateNode.fields, + }, + format: '', + relationTo: slateNode.relationTo, + type: 'upload', + value: { + id: slateNode.value?.id || '', + }, + version: 1, + } as const as SerializedUploadNode + }, + nodeTypes: ['upload'], +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/defaultConverters.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/defaultConverters.ts new file mode 100644 index 000000000..d0f5ff75b --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/defaultConverters.ts @@ -0,0 +1,23 @@ +import type { SlateNodeConverter } from './types' + +import { HeadingConverter } from './converters/heading' +import { IndentConverter } from './converters/indent' +import { LinkConverter } from './converters/link' +import { ListItemConverter } from './converters/listItem' +import { OrderedListConverter } from './converters/orderedList' +import { RelationshipConverter } from './converters/relationship' +import { UnknownConverter } from './converters/unknown' +import { UnorderedListConverter } from './converters/unorderedList' +import { UploadConverter } from './converters/upload' + +export const defaultConverters: SlateNodeConverter[] = [ + UnknownConverter, + UploadConverter, + UnorderedListConverter, + OrderedListConverter, + RelationshipConverter, + ListItemConverter, + LinkConverter, + HeadingConverter, + IndentConverter, +] diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/index.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/index.ts new file mode 100644 index 000000000..bf52b2ee4 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/index.ts @@ -0,0 +1,137 @@ +import type { + SerializedEditorState, + SerializedLexicalNode, + SerializedParagraphNode, + SerializedTextNode, +} from 'lexical' + +import type { SlateNode, SlateNodeConverter } from './types' + +import { NodeFormat } from '../../../../lexical/utils/nodeFormat' + +export function convertSlateToLexical({ + converters, + slateData, +}: { + converters: SlateNodeConverter[] + slateData: SlateNode[] +}): SerializedEditorState { + return { + root: { + children: convertSlateNodesToLexical({ + canContainParagraphs: true, + converters, + parentNodeType: 'root', + slateNodes: slateData, + }), + direction: 'ltr', + format: '', + indent: 0, + type: 'root', + version: 1, + }, + } +} + +export function convertSlateNodesToLexical({ + canContainParagraphs, + converters, + parentNodeType, + slateNodes, +}: { + canContainParagraphs: boolean + converters: SlateNodeConverter[] + /** + * Type of the parent lexical node (not the type of the original, parent slate type) + */ + parentNodeType: string + slateNodes: SlateNode[] +}): SerializedLexicalNode[] { + const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown')) + return ( + slateNodes.map((slateNode, i) => { + if (!('type' in slateNode)) { + if (canContainParagraphs) { + // This is a paragraph node. They do not have a type property in Slate + return convertParagraphNode(converters, slateNode) + } else { + // This is a simple text node. canContainParagraphs may be false if this is nested inside of a paragraph already, since paragraphs cannot contain paragraphs + return convertTextNode(slateNode) + } + } + if (slateNode.type === 'p') { + return convertParagraphNode(converters, slateNode) + } + + const converter = converters.find((converter) => converter.nodeTypes.includes(slateNode.type)) + + if (converter) { + return converter.converter({ childIndex: i, converters, parentNodeType, slateNode }) + } + + console.warn('slateToLexical > No converter found for node type: ' + slateNode.type) + return unknownConverter?.converter({ + childIndex: i, + converters, + parentNodeType, + slateNode, + }) + }) || [] + ) +} + +export function convertParagraphNode( + converters: SlateNodeConverter[], + node: SlateNode, +): SerializedParagraphNode { + return { + children: convertSlateNodesToLexical({ + canContainParagraphs: false, + converters, + parentNodeType: 'paragraph', + slateNodes: node.children || [], + }), + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + } +} +export function convertTextNode(node: SlateNode): SerializedTextNode { + return { + detail: 0, + format: convertNodeToFormat(node), + mode: 'normal', + style: '', + text: node.text, + type: 'text', + version: 1, + } +} + +export function convertNodeToFormat(node: SlateNode): number { + let format = 0 + if (node.bold) { + format = format | NodeFormat.IS_BOLD + } + if (node.italic) { + format = format | NodeFormat.IS_ITALIC + } + if (node.strikethrough) { + format = format | NodeFormat.IS_STRIKETHROUGH + } + if (node.underline) { + format = format | NodeFormat.IS_UNDERLINE + } + if (node.subscript) { + format = format | NodeFormat.IS_SUBSCRIPT + } + if (node.superscript) { + format = format | NodeFormat.IS_SUPERSCRIPT + } + if (node.code) { + format = format | NodeFormat.IS_CODE + } + return format +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/types.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/types.ts new file mode 100644 index 000000000..3710efe87 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/converter/types.ts @@ -0,0 +1,22 @@ +import type { SerializedLexicalNode } from 'lexical' + +export type SlateNodeConverter = { + converter: ({ + childIndex, + converters, + parentNodeType, + slateNode, + }: { + childIndex: number + converters: SlateNodeConverter[] + parentNodeType: string + slateNode: SlateNode + }) => T + nodeTypes: string[] +} + +export type SlateNode = { + [key: string]: any + children?: SlateNode[] + type?: string // doesn't always have type, e.g. for paragraphs +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/index.ts b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/index.ts new file mode 100644 index 000000000..6dc9ff010 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/index.ts @@ -0,0 +1,56 @@ +import type { FeatureProvider } from '../../types' +import type { SlateNodeConverter } from './converter/types' + +import { convertSlateToLexical } from './converter' +import { defaultConverters } from './converter/defaultConverters' +import { UnknownConvertedNode } from './nodes/unknownConvertedNode' + +type Props = { + converters?: + | (({ defaultConverters }: { defaultConverters: SlateNodeConverter[] }) => SlateNodeConverter[]) + | SlateNodeConverter[] +} + +export const SlateToLexicalFeature = (props?: Props): FeatureProvider => { + if (!props) { + props = {} + } + + props.converters = + props?.converters && typeof props?.converters === 'function' + ? props.converters({ defaultConverters: defaultConverters }) + : (props?.converters as SlateNodeConverter[]) || defaultConverters + + return { + feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { + return { + hooks: { + load({ incomingEditorState }) { + if ( + !incomingEditorState || + !Array.isArray(incomingEditorState) || + 'root' in incomingEditorState + ) { + // incomingEditorState null or not from Slate + return incomingEditorState + } + // Slate => convert to lexical + + return convertSlateToLexical({ + converters: props.converters as SlateNodeConverter[], + slateData: incomingEditorState, + }) + }, + }, + nodes: [ + { + node: UnknownConvertedNode, + type: UnknownConvertedNode.getType(), + }, + ], + props, + } + }, + key: 'slateToLexical', + } +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/nodes/unknownConvertedNode/index.scss b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/nodes/unknownConvertedNode/index.scss new file mode 100644 index 000000000..7eedad732 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/nodes/unknownConvertedNode/index.scss @@ -0,0 +1,16 @@ +@import 'payload/scss'; + +span.unknownConverted { + text-transform: uppercase; + font-family: 'Roboto Mono', monospace; + letter-spacing: 2px; + font-size: base(0.5); + margin: 0 0 base(1); + background: red; + color: white; + display: inline-block; + + div { + background: red; + } +} diff --git a/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/nodes/unknownConvertedNode/index.tsx b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/nodes/unknownConvertedNode/index.tsx new file mode 100644 index 000000000..dffbc0cae --- /dev/null +++ b/packages/richtext-lexical/src/field/features/migrations/SlateToLexical/nodes/unknownConvertedNode/index.tsx @@ -0,0 +1,97 @@ +import type { SerializedLexicalNode, Spread } from 'lexical' + +import { addClassNamesToElement } from '@lexical/utils' +import { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical' +import React from 'react' + +import './index.scss' + +export type UnknownConvertedNodeData = { + nodeData: unknown + nodeType: string +} + +export type SerializedUnknownConvertedNode = Spread< + { + data: UnknownConvertedNodeData + }, + SerializedLexicalNode +> + +/** @noInheritDoc */ +export class UnknownConvertedNode extends DecoratorNode { + __data: UnknownConvertedNodeData + + constructor({ data, key }: { data: UnknownConvertedNodeData; key?: NodeKey }) { + super(key) + this.__data = data + } + + static clone(node: UnknownConvertedNode): UnknownConvertedNode { + return new UnknownConvertedNode({ + data: node.__data, + key: node.__key, + }) + } + + static getType(): string { + return 'unknownConverted' + } + + static importJSON(serializedNode: SerializedUnknownConvertedNode): UnknownConvertedNode { + const node = $createUnknownConvertedNode({ data: serializedNode.data }) + return node + } + + canInsertTextAfter(): true { + return true + } + + canInsertTextBefore(): true { + return true + } + + createDOM(config: EditorConfig): HTMLElement { + const element = document.createElement('span') + addClassNamesToElement(element, 'unknownConverted') + return element + } + + decorate(): JSX.Element | null { + return
Unknown converted Slate node: {this.__data?.nodeType}
+ } + + exportJSON(): SerializedUnknownConvertedNode { + return { + data: this.__data, + type: this.getType(), + version: 1, + } + } + + // Mutation + + isInline(): boolean { + return true + } + + updateDOM(prevNode: UnknownConvertedNode, dom: HTMLElement): boolean { + return false + } +} + +export function $createUnknownConvertedNode({ + data, +}: { + data: UnknownConvertedNodeData +}): UnknownConvertedNode { + return new UnknownConvertedNode({ + data, + }) +} + +export function $isUnknownConvertedNode( + node: LexicalNode | null | undefined, +): node is UnknownConvertedNode { + return node instanceof UnknownConvertedNode +} diff --git a/packages/richtext-lexical/src/field/features/types.ts b/packages/richtext-lexical/src/field/features/types.ts index cfbaccd0d..0277bc6fe 100644 --- a/packages/richtext-lexical/src/field/features/types.ts +++ b/packages/richtext-lexical/src/field/features/types.ts @@ -24,7 +24,7 @@ export type AfterReadPromise> currentDepth: number depth: number - field: RichTextField + field: RichTextField node: T overrideAccess: boolean req: PayloadRequest @@ -51,6 +51,18 @@ export type Feature = { floatingSelectToolbar?: { sections: FloatingToolbarSection[] } + hooks?: { + load?: ({ + incomingEditorState, + }: { + incomingEditorState: SerializedEditorState + }) => SerializedEditorState + save?: ({ + incomingEditorState, + }: { + incomingEditorState: SerializedEditorState + }) => SerializedEditorState + } markdownTransformers?: Transformer[] nodes?: Array<{ afterReadPromises?: Array @@ -123,6 +135,22 @@ export type SanitizedFeatures = Required< floatingSelectToolbar: { sections: FloatingToolbarSection[] } + hooks: { + load: Array< + ({ + incomingEditorState, + }: { + incomingEditorState: SerializedEditorState + }) => SerializedEditorState + > + save: Array< + ({ + incomingEditorState, + }: { + incomingEditorState: SerializedEditorState + }) => SerializedEditorState + > + } plugins?: Array< | { // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality diff --git a/packages/richtext-lexical/src/field/index.scss b/packages/richtext-lexical/src/field/index.scss index 59ede143f..2a4e8bbb9 100644 --- a/packages/richtext-lexical/src/field/index.scss +++ b/packages/richtext-lexical/src/field/index.scss @@ -4,6 +4,12 @@ display: flex; isolation: isolate; + .errorBoundary { + pre { + text-wrap: unset; + } + } + &__wrap { width: 100%; position: relative; diff --git a/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx b/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx index 9f4cbe9d1..2691346f2 100644 --- a/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx +++ b/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx @@ -87,13 +87,17 @@ export const LexicalEditor: React.FC = (props) => { return } })} + {editor.isEditable() && ( + + + + + )} )} {editor.isEditable() && ( - - )} diff --git a/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx b/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx index 75a6f2095..9eb10a7a3 100644 --- a/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx +++ b/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx @@ -21,7 +21,31 @@ export type LexicalProviderProps = { value: SerializedEditorState } export const LexicalProvider: React.FC = (props) => { - const { editorConfig, fieldProps, initialState, onChange, readOnly, setValue, value } = props + const { editorConfig, fieldProps, onChange, readOnly, setValue } = props + let { initialState, value } = props + + // Transform initialState through load hooks + if (editorConfig?.features?.hooks?.load?.length) { + editorConfig.features.hooks.load.forEach((hook) => { + initialState = hook({ incomingEditorState: initialState }) + value = hook({ incomingEditorState: value }) + }) + } + + if ( + (value && Array.isArray(value) && !('root' in value)) || + (initialState && Array.isArray(initialState) && !('root' in initialState)) + ) { + throw new Error( + 'You have tried to pass in data from the old, Slate editor, to the new, Lexical editor. This is not supported. There is no automatic conversion from Slate to Lexical data available yet (coming soon). Please remove the data from the field and start again.', + ) + } + + if (value && 'jsonContent' in value) { + throw new Error( + 'You have tried to pass in data from payload-plugin-lexical. This is not supported. The data structure has changed in this editor, compared to the plugin, and there is no automatic conversion available yet (coming soon). Please remove the data from the field and start again.', + ) + } const initialConfig: InitialConfigType = { editable: readOnly === true ? false : true, diff --git a/packages/richtext-lexical/src/field/lexical/config/sanitize.ts b/packages/richtext-lexical/src/field/lexical/config/sanitize.ts index cf39b20c4..ad793c0a0 100644 --- a/packages/richtext-lexical/src/field/lexical/config/sanitize.ts +++ b/packages/richtext-lexical/src/field/lexical/config/sanitize.ts @@ -10,6 +10,10 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature floatingSelectToolbar: { sections: [], }, + hooks: { + load: [], + save: [], + }, markdownTransformers: [], nodes: [], plugins: [], @@ -21,6 +25,15 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature } features.forEach((feature) => { + if (feature.hooks) { + if (feature.hooks?.load?.length) { + sanitized.hooks.load = sanitized.hooks.load.concat(feature.hooks.load) + } + if (feature.hooks?.save?.length) { + sanitized.hooks.save = sanitized.hooks.save.concat(feature.hooks.save) + } + } + if (feature.nodes?.length) { sanitized.nodes = sanitized.nodes.concat(feature.nodes) feature.nodes.forEach((node) => { diff --git a/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/DropDown.tsx b/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/DropDown.tsx index 99cdaa488..fd9b928ca 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/DropDown.tsx +++ b/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/DropDown.tsx @@ -99,6 +99,11 @@ export function DropDownItem({ }) } }} + onMouseDown={(e) => { + // This is required for Firefox compatibility. Without it, the dropdown will disappear without the onClick being called. + // This only happens in Firefox. Must be something about how Firefox handles focus events differently. + e.preventDefault() + }} ref={ref} title={title} type="button" diff --git a/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/index.tsx b/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/index.tsx index 595bdc876..bcb956d1c 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/index.tsx +++ b/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/index.tsx @@ -15,7 +15,6 @@ import { createPortal } from 'react-dom' import { useEditorConfigContext } from '../../config/EditorConfigProvider' import { getDOMRangeRect } from '../../utils/getDOMRangeRect' -import { getSelectedNode } from '../../utils/getSelectedNode' import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition' import { ToolbarButton } from './ToolbarButton' import { ToolbarDropdown } from './ToolbarDropdown' @@ -33,28 +32,41 @@ function FloatingSelectToolbar({ const { editorConfig } = useEditorConfigContext() - function mouseMoveListener(e: MouseEvent) { - if (popupCharStylesEditorRef?.current && (e.buttons === 1 || e.buttons === 3)) { + const closeFloatingToolbar = useCallback(() => { + if (popupCharStylesEditorRef?.current) { const isOpacityZero = popupCharStylesEditorRef.current.style.opacity === '0' const isPointerEventsNone = popupCharStylesEditorRef.current.style.pointerEvents === 'none' - if (!isOpacityZero || !isPointerEventsNone) { - // Check if the mouse is not over the popup - const x = e.clientX - const y = e.clientY - const elementUnderMouse = document.elementFromPoint(x, y) - if (!popupCharStylesEditorRef.current.contains(elementUnderMouse)) { - // Mouse is not over the target element => not a normal click, but probably a drag - if (!isOpacityZero) { - popupCharStylesEditorRef.current.style.opacity = '0' - } - if (!isPointerEventsNone) { - popupCharStylesEditorRef.current.style.pointerEvents = 'none' + + if (!isOpacityZero) { + popupCharStylesEditorRef.current.style.opacity = '0' + } + if (!isPointerEventsNone) { + popupCharStylesEditorRef.current.style.pointerEvents = 'none' + } + } + }, [popupCharStylesEditorRef]) + + const mouseMoveListener = useCallback( + (e: MouseEvent) => { + if (popupCharStylesEditorRef?.current && (e.buttons === 1 || e.buttons === 3)) { + const isOpacityZero = popupCharStylesEditorRef.current.style.opacity === '0' + const isPointerEventsNone = popupCharStylesEditorRef.current.style.pointerEvents === 'none' + if (!isOpacityZero || !isPointerEventsNone) { + // Check if the mouse is not over the popup + const x = e.clientX + const y = e.clientY + const elementUnderMouse = document.elementFromPoint(x, y) + if (!popupCharStylesEditorRef.current.contains(elementUnderMouse)) { + // Mouse is not over the target element => not a normal click, but probably a drag + closeFloatingToolbar() } } } - } - } - function mouseUpListener(e: MouseEvent) { + }, + [closeFloatingToolbar], + ) + + const mouseUpListener = useCallback(() => { if (popupCharStylesEditorRef?.current) { if (popupCharStylesEditorRef.current.style.opacity !== '1') { popupCharStylesEditorRef.current.style.opacity = '1' @@ -63,7 +75,7 @@ function FloatingSelectToolbar({ popupCharStylesEditorRef.current.style.pointerEvents = 'auto' } } - } + }, [popupCharStylesEditorRef]) useEffect(() => { document.addEventListener('mousemove', mouseMoveListener) @@ -73,7 +85,7 @@ function FloatingSelectToolbar({ document.removeEventListener('mousemove', mouseMoveListener) document.removeEventListener('mouseup', mouseUpListener) } - }, [popupCharStylesEditorRef]) + }, [popupCharStylesEditorRef, mouseMoveListener, mouseUpListener]) const updateTextFormatFloatingToolbar = useCallback(() => { const selection = $getSelection() diff --git a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx index 8153fc27a..beab301d9 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx +++ b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx @@ -503,6 +503,7 @@ export function LexicalMenu({ } export function useMenuAnchorRef( + anchorElem: HTMLElement, resolution: MenuResolution | null, setResolution: (r: MenuResolution | null) => void, className?: string, @@ -517,8 +518,11 @@ export function useMenuAnchorRef( const menuEle = containerDiv.firstChild as Element if (rootElement !== null && resolution !== null) { - const { height, left, top, width } = resolution.getRect() - containerDiv.style.top = `${top + window.scrollY + VERTICAL_OFFSET}px` + let { height, left, top, width } = resolution.getRect() + const rawTop = top + const rawLeft = left + top -= anchorElem.getBoundingClientRect().top + window.scrollY + left -= anchorElem.getBoundingClientRect().left + window.scrollX containerDiv.style.left = `${left + window.scrollX}px` containerDiv.style.height = `${height}px` containerDiv.style.width = `${width}px` @@ -533,19 +537,18 @@ export function useMenuAnchorRef( containerDiv.style.left = `${rootElementRect.right - menuWidth + window.scrollX}px` } - const wouldGoOffTopOfScreen = top < menuHeight - const wouldGoOffBottomOfContainer = top + menuHeight > rootElementRect.bottom + const wouldGoOffBottomOfScreen = rawTop + menuHeight + VERTICAL_OFFSET > window.innerHeight + //const wouldGoOffBottomOfContainer = top + menuHeight > rootElementRect.bottom + const wouldGoOffTopOfScreen = rawTop < 0 // Position slash menu above the cursor instead of below (default) if it would otherwise go off the bottom of the screen. - if ( - (top + menuHeight > window.innerHeight || - (wouldGoOffBottomOfContainer && !wouldGoOffTopOfScreen)) && - top - rootElementRect.top > menuHeight - ) { + if (wouldGoOffBottomOfScreen && !wouldGoOffTopOfScreen) { const margin = 24 containerDiv.style.top = `${ top + VERTICAL_OFFSET - menuHeight + window.scrollY - (height + margin) }px` + } else { + containerDiv.style.top = `${top + window.scrollY + VERTICAL_OFFSET}px` } } @@ -558,12 +561,12 @@ export function useMenuAnchorRef( containerDiv.setAttribute('role', 'listbox') containerDiv.style.display = 'block' containerDiv.style.position = 'absolute' - document.body.append(containerDiv) + anchorElem.append(containerDiv) } anchorElementRef.current = containerDiv rootElement.setAttribute('aria-controls', 'typeahead-menu') } - }, [editor, resolution, className]) + }, [editor, resolution, className, anchorElem]) useEffect(() => { const rootElement = editor.getRootElement() diff --git a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index.tsx b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index.tsx index 3e13b6f2e..209accc18 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index.tsx +++ b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index.tsx @@ -168,6 +168,7 @@ export function useBasicTypeaheadTriggerMatch( export type TypeaheadMenuPluginProps = { anchorClassName?: string + anchorElem: HTMLElement groupsWithOptions: Array menuRenderFn: MenuRenderFn onClose?: () => void @@ -188,6 +189,7 @@ export const ENABLE_SLASH_MENU_COMMAND: LexicalCommand<{ export function LexicalTypeaheadMenuPlugin({ anchorClassName, + anchorElem, groupsWithOptions, menuRenderFn, onClose, @@ -198,7 +200,7 @@ export function LexicalTypeaheadMenuPlugin({ }: TypeaheadMenuPluginProps): JSX.Element | null { const [editor] = useLexicalComposerContext() const [resolution, setResolution] = useState(null) - const anchorElementRef = useMenuAnchorRef(resolution, setResolution, anchorClassName) + const anchorElementRef = useMenuAnchorRef(anchorElem, resolution, setResolution, anchorClassName) const closeTypeahead = useCallback(() => { setResolution(null) diff --git a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.scss b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.scss index dae4953e4..352edca09 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.scss +++ b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.scss @@ -15,6 +15,8 @@ html[data-theme='light'] { font-family: var(--font-body); max-height: 300px; overflow-y: scroll; + z-index: 10; + position: absolute; .group { padding-bottom: 8px; diff --git a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx index ed65d1987..a817ec631 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx +++ b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx @@ -64,7 +64,11 @@ function SlashMenuItem({ ) } -export function SlashMenuPlugin(): JSX.Element { +export function SlashMenuPlugin({ + anchorElem = document.body, +}: { + anchorElem?: HTMLElement +}): JSX.Element { const [editor] = useLexicalComposerContext() const [queryString, setQueryString] = useState(null) const { editorConfig } = useEditorConfigContext() @@ -162,6 +166,7 @@ export function SlashMenuPlugin(): JSX.Element { return ( { @@ -60,11 +59,9 @@ function hideTargetLine( ) { if (targetLineElem) { targetLineElem.style.opacity = '0' - targetLineElem.style.transform = 'translate(-10000px, -10000px)' } if (lastTargetBlockElem) { lastTargetBlockElem.style.opacity = '1' - lastTargetBlockElem.style.transform = 'translate(0, 0)' //lastTargetBlockElem.style.border = 'none' } } diff --git a/packages/richtext-lexical/src/field/lexical/ui/ContentEditable.scss b/packages/richtext-lexical/src/field/lexical/ui/ContentEditable.scss index 79ac0fcf7..712d023a4 100644 --- a/packages/richtext-lexical/src/field/lexical/ui/ContentEditable.scss +++ b/packages/richtext-lexical/src/field/lexical/ui/ContentEditable.scss @@ -13,7 +13,7 @@ & > * { transition: transform 0.2s ease-in-out; - will-change: transform; + // will-change: transform; // breaks cursor rendering for empty paragraph blocks in safari, and creates other issues position: relative; // makes sure that z-index properties work - for example for the Blocks feature } } diff --git a/packages/richtext-lexical/src/field/lexical/utils/nodeFormat.ts b/packages/richtext-lexical/src/field/lexical/utils/nodeFormat.ts new file mode 100644 index 000000000..5cb26c167 --- /dev/null +++ b/packages/richtext-lexical/src/field/lexical/utils/nodeFormat.ts @@ -0,0 +1,124 @@ +/* eslint-disable perfectionist/sort-objects */ +/* eslint-disable regexp/no-obscure-range */ +/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ +//This copy-and-pasted from lexical here here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts + +import type { ElementFormatType, TextFormatType } from 'lexical' +import type { TextDetailType, TextModeType } from 'lexical/nodes/LexicalTextNode' + +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +// DOM +export const NodeFormat = { + DOM_ELEMENT_TYPE: 1, + DOM_TEXT_TYPE: 3, + // Reconciling + NO_DIRTY_NODES: 0, + HAS_DIRTY_NODES: 1, + FULL_RECONCILE: 2, + // Text node modes + IS_NORMAL: 0, + IS_TOKEN: 1, + IS_SEGMENTED: 2, + IS_INERT: 3, + // Text node formatting + IS_BOLD: 1, + IS_ITALIC: 1 << 1, + IS_STRIKETHROUGH: 1 << 2, + IS_UNDERLINE: 1 << 3, + IS_CODE: 1 << 4, + IS_SUBSCRIPT: 1 << 5, + IS_SUPERSCRIPT: 1 << 6, + IS_HIGHLIGHT: 1 << 7, + // Text node details + IS_DIRECTIONLESS: 1, + IS_UNMERGEABLE: 1 << 1, + // Element node formatting + IS_ALIGN_LEFT: 1, + IS_ALIGN_CENTER: 2, + IS_ALIGN_RIGHT: 3, + IS_ALIGN_JUSTIFY: 4, + IS_ALIGN_START: 5, + IS_ALIGN_END: 6, +} as const + +export const IS_ALL_FORMATTING = + NodeFormat.IS_BOLD | + NodeFormat.IS_ITALIC | + NodeFormat.IS_STRIKETHROUGH | + NodeFormat.IS_UNDERLINE | + NodeFormat.IS_CODE | + NodeFormat.IS_SUBSCRIPT | + NodeFormat.IS_SUPERSCRIPT | + NodeFormat.IS_HIGHLIGHT + +// Reconciliation +export const NON_BREAKING_SPACE = '\u00A0' + +export const DOUBLE_LINE_BREAK = '\n\n' + +// For FF, we need to use a non-breaking space, or it gets composition +// in a stuck state. + +const RTL = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC' +const LTR = + 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6' + + '\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C' + + '\uFE00-\uFE6F\uFEFD-\uFFFF' + +// eslint-disable-next-line no-misleading-character-class +export const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']') +// eslint-disable-next-line no-misleading-character-class +export const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']') + +export const TEXT_TYPE_TO_FORMAT: Record = { + bold: NodeFormat.IS_BOLD, + code: NodeFormat.IS_CODE, + highlight: NodeFormat.IS_HIGHLIGHT, + italic: NodeFormat.IS_ITALIC, + strikethrough: NodeFormat.IS_STRIKETHROUGH, + subscript: NodeFormat.IS_SUBSCRIPT, + superscript: NodeFormat.IS_SUPERSCRIPT, + underline: NodeFormat.IS_UNDERLINE, +} + +export const DETAIL_TYPE_TO_DETAIL: Record = { + directionless: NodeFormat.IS_DIRECTIONLESS, + unmergeable: NodeFormat.IS_UNMERGEABLE, +} + +export const ELEMENT_TYPE_TO_FORMAT: Record, number> = { + center: NodeFormat.IS_ALIGN_CENTER, + end: NodeFormat.IS_ALIGN_END, + justify: NodeFormat.IS_ALIGN_JUSTIFY, + left: NodeFormat.IS_ALIGN_LEFT, + right: NodeFormat.IS_ALIGN_RIGHT, + start: NodeFormat.IS_ALIGN_START, +} + +export const ELEMENT_FORMAT_TO_TYPE: Record = { + [NodeFormat.IS_ALIGN_CENTER]: 'center', + [NodeFormat.IS_ALIGN_END]: 'end', + [NodeFormat.IS_ALIGN_JUSTIFY]: 'justify', + [NodeFormat.IS_ALIGN_LEFT]: 'left', + [NodeFormat.IS_ALIGN_RIGHT]: 'right', + [NodeFormat.IS_ALIGN_START]: 'start', +} + +export const TEXT_MODE_TO_TYPE: Record = { + normal: NodeFormat.IS_NORMAL, + segmented: NodeFormat.IS_SEGMENTED, + token: NodeFormat.IS_TOKEN, +} + +export const TEXT_TYPE_TO_MODE: Record = { + [NodeFormat.IS_NORMAL]: 'normal', + [NodeFormat.IS_SEGMENTED]: 'segmented', + [NodeFormat.IS_TOKEN]: 'token', +} diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index 36d03005e..b74d97ae8 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -1,3 +1,4 @@ +import type { SerializedEditorState } from 'lexical' import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor' import type { RichTextAdapter } from 'payload/types' @@ -26,8 +27,10 @@ export type LexicalEditorProps = { lexical?: LexicalEditorConfig } -export function lexicalEditor(props?: LexicalEditorProps): RichTextAdapter { - let finalSanitizedEditorConfig: SanitizedEditorConfig = null +export function lexicalEditor( + props?: LexicalEditorProps, +): RichTextAdapter { + let finalSanitizedEditorConfig: SanitizedEditorConfig if (!props || (!props.features && !props.lexical)) { finalSanitizedEditorConfig = cloneDeep(defaultSanitizedEditorConfig) } else { @@ -150,6 +153,7 @@ export { IndentFeature } from './field/features/indent' export { CheckListFeature } from './field/features/lists/CheckList' export { OrderedListFeature } from './field/features/lists/OrderedList' export { UnoderedListFeature } from './field/features/lists/UnorderedList' +export { SlateToLexicalFeature } from './field/features/migrations/SlateToLexical' export type { AfterReadPromise, Feature, @@ -198,6 +202,20 @@ export { isHTMLElement } from './field/lexical/utils/guard' export { invariant } from './field/lexical/utils/invariant' export { joinClasses } from './field/lexical/utils/joinClasses' export { createBlockNode } from './field/lexical/utils/markdown/createBlockNode' +export { + DETAIL_TYPE_TO_DETAIL, + DOUBLE_LINE_BREAK, + ELEMENT_FORMAT_TO_TYPE, + ELEMENT_TYPE_TO_FORMAT, + IS_ALL_FORMATTING, + LTR_REGEX, + NON_BREAKING_SPACE, + NodeFormat, + RTL_REGEX, + TEXT_MODE_TO_TYPE, + TEXT_TYPE_TO_FORMAT, + TEXT_TYPE_TO_MODE, +} from './field/lexical/utils/nodeFormat' export { Point, isPoint } from './field/lexical/utils/point' export { Rect } from './field/lexical/utils/rect' export { setFloatingElemPosition } from './field/lexical/utils/setFloatingElemPosition' diff --git a/packages/richtext-lexical/src/populate/populate.ts b/packages/richtext-lexical/src/populate/populate.ts index 2ec6e4dd0..5c0ab42bd 100644 --- a/packages/richtext-lexical/src/populate/populate.ts +++ b/packages/richtext-lexical/src/populate/populate.ts @@ -1,3 +1,4 @@ +import type { SerializedEditorState } from 'lexical' import type { PayloadRequest } from 'payload/types' import type { Collection, Field, RichTextField } from 'payload/types' @@ -7,7 +8,7 @@ type Arguments = { currentDepth?: number data: unknown depth: number - field: RichTextField + field: RichTextField key: number | string overrideAccess?: boolean req: PayloadRequest diff --git a/packages/richtext-lexical/src/populate/recurseNestedFields.ts b/packages/richtext-lexical/src/populate/recurseNestedFields.ts index eba061ec2..65846d126 100644 --- a/packages/richtext-lexical/src/populate/recurseNestedFields.ts +++ b/packages/richtext-lexical/src/populate/recurseNestedFields.ts @@ -173,7 +173,7 @@ export const recurseNestedFields = ({ promises, req, showHiddenFields, - siblingDoc, + siblingDoc: data[field.name][i], // This has to be scoped to the blocks's fields, otherwise there may be population issues, e.g. for a relationship field with Blocks Node, with a Blocks Field, with a RichText Field, With Relationship Node. The last richtext field would try to find itself using siblingDoc[field.nane], which only works if the siblingDoc is scoped to the blocks's fields }) } }) @@ -191,14 +191,13 @@ export const recurseNestedFields = ({ promises, req, showHiddenFields, - siblingDoc, + siblingDoc, // TODO: if there's any population issues, this might have to be data[field.name][i] as well }) }) } } if (field.type === 'richText') { - // TODO: This does not properly work yet. E.g. it does not handle a relationship inside of lexical inside of block inside of lexical const editor: RichTextAdapter = field?.editor if (editor?.afterReadPromise) { diff --git a/packages/richtext-lexical/src/populate/richTextRelationshipPromise.ts b/packages/richtext-lexical/src/populate/richTextRelationshipPromise.ts index d4ce2d5d8..198b5a6cd 100644 --- a/packages/richtext-lexical/src/populate/richTextRelationshipPromise.ts +++ b/packages/richtext-lexical/src/populate/richTextRelationshipPromise.ts @@ -4,7 +4,9 @@ import type { PayloadRequest, RichTextAdapter, RichTextField } from 'payload/typ import type { AfterReadPromise } from '../field/features/types' import type { AdapterProps } from '../types' -export type Args = Parameters['afterReadPromise']>[0] & { +export type Args = Parameters< + RichTextAdapter['afterReadPromise'] +>[0] & { afterReadPromises: Map> } @@ -13,7 +15,7 @@ type RecurseRichTextArgs = { children: SerializedLexicalNode[] currentDepth: number depth: number - field: RichTextField + field: RichTextField overrideAccess: boolean promises: Promise[] req: PayloadRequest @@ -57,7 +59,7 @@ export const recurseRichText = ({ } } - if ('children' in node && node?.children) { + if ('children' in node && Array.isArray(node?.children) && node?.children?.length) { recurseRichText({ afterReadPromises, children: node.children as SerializedLexicalNode[], diff --git a/packages/richtext-lexical/src/types.ts b/packages/richtext-lexical/src/types.ts index 2730385bb..cb2ca57f0 100644 --- a/packages/richtext-lexical/src/types.ts +++ b/packages/richtext-lexical/src/types.ts @@ -1,10 +1,11 @@ +import type { SerializedEditorState } from 'lexical' import type { FieldPermissions } from 'payload/auth' import type { FieldTypes } from 'payload/config' import type { RichTextFieldProps } from 'payload/types' import type { SanitizedEditorConfig } from './field/lexical/config/types' -export type FieldProps = RichTextFieldProps & { +export type FieldProps = RichTextFieldProps & { fieldTypes: FieldTypes indexPath: string path?: string diff --git a/packages/richtext-slate/package.json b/packages/richtext-slate/package.json index 19d8520fb..9d9f5c1c2 100644 --- a/packages/richtext-slate/package.json +++ b/packages/richtext-slate/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/richtext-slate", - "version": "1.0.2", + "version": "1.0.3", "description": "The officially supported Slate richtext adapter for Payload", "repository": "https://github.com/payloadcms/payload", "license": "MIT", diff --git a/packages/richtext-slate/src/cell/index.tsx b/packages/richtext-slate/src/cell/index.tsx index 67552d1df..f2c9eb016 100644 --- a/packages/richtext-slate/src/cell/index.tsx +++ b/packages/richtext-slate/src/cell/index.tsx @@ -5,7 +5,7 @@ import React from 'react' import type { AdapterArguments } from '../types' -const RichTextCell: React.FC, any>> = ({ +const RichTextCell: React.FC, any>> = ({ data, }) => { const flattenedText = data?.map((i) => i?.children?.map((c) => c.text)).join(' ') diff --git a/packages/richtext-slate/src/data/populate.ts b/packages/richtext-slate/src/data/populate.ts index e1a39961e..16eb7acf5 100644 --- a/packages/richtext-slate/src/data/populate.ts +++ b/packages/richtext-slate/src/data/populate.ts @@ -7,7 +7,7 @@ type Arguments = { currentDepth?: number data: unknown depth: number - field: RichTextField + field: RichTextField key: number | string overrideAccess?: boolean req: PayloadRequest diff --git a/packages/richtext-slate/src/data/richTextRelationshipPromise.ts b/packages/richtext-slate/src/data/richTextRelationshipPromise.ts index 25a01325a..dbc3f8dab 100644 --- a/packages/richtext-slate/src/data/richTextRelationshipPromise.ts +++ b/packages/richtext-slate/src/data/richTextRelationshipPromise.ts @@ -5,13 +5,13 @@ import type { AdapterArguments } from '../types' import { populate } from './populate' import { recurseNestedFields } from './recurseNestedFields' -export type Args = Parameters['afterReadPromise']>[0] +export type Args = Parameters['afterReadPromise']>[0] type RecurseRichTextArgs = { children: unknown[] currentDepth: number depth: number - field: RichTextField + field: RichTextField overrideAccess: boolean promises: Promise[] req: PayloadRequest diff --git a/packages/richtext-slate/src/data/validation.ts b/packages/richtext-slate/src/data/validation.ts index e319851e0..f0483dc1f 100644 --- a/packages/richtext-slate/src/data/validation.ts +++ b/packages/richtext-slate/src/data/validation.ts @@ -7,8 +7,8 @@ import { defaultRichTextValue } from './defaultValue' export const richTextValidate: Validate< unknown, unknown, - RichTextField, - RichTextField + RichTextField, + RichTextField > = (value, { required, t }) => { if (required) { const stringifiedDefaultValue = JSON.stringify(defaultRichTextValue) diff --git a/packages/richtext-slate/src/index.ts b/packages/richtext-slate/src/index.ts index a8fa8bb86..4adfb9cd2 100644 --- a/packages/richtext-slate/src/index.ts +++ b/packages/richtext-slate/src/index.ts @@ -9,7 +9,7 @@ import { richTextRelationshipPromise } from './data/richTextRelationshipPromise' import { richTextValidate } from './data/validation' import RichTextField from './field' -export function slateEditor(args: AdapterArguments): RichTextAdapter { +export function slateEditor(args: AdapterArguments): RichTextAdapter { return { CellComponent: withMergedProps({ Component: RichTextCell, diff --git a/packages/richtext-slate/src/types.ts b/packages/richtext-slate/src/types.ts index 87969f42a..f64f2842e 100644 --- a/packages/richtext-slate/src/types.ts +++ b/packages/richtext-slate/src/types.ts @@ -73,4 +73,4 @@ export type AdapterArguments = { } } -export type FieldProps = RichTextFieldProps +export type FieldProps = RichTextFieldProps diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f892a866d..97040eddf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,6 +141,9 @@ importers: shelljs: specifier: 0.8.5 version: 0.8.5 + simple-git: + specifier: ^3.20.0 + version: 3.20.0 slash: specifier: 3.0.0 version: 3.0.0 @@ -309,6 +312,67 @@ importers: specifier: workspace:* version: link:../payload + packages/create-payload-app: + dependencies: + '@sindresorhus/slugify': + specifier: ^1.1.0 + version: 1.1.2 + arg: + specifier: ^5.0.0 + version: 5.0.2 + chalk: + specifier: ^4.1.0 + version: 4.1.2 + command-exists: + specifier: ^1.2.9 + version: 1.2.9 + degit: + specifier: ^2.8.4 + version: 2.8.4 + execa: + specifier: ^5.0.0 + version: 5.1.1 + figures: + specifier: ^3.2.0 + version: 3.2.0 + fs-extra: + specifier: ^9.0.1 + version: 9.1.0 + handlebars: + specifier: ^4.7.7 + version: 4.7.8 + ora: + specifier: ^5.1.0 + version: 5.4.1 + prompts: + specifier: ^2.4.2 + version: 2.4.2 + terminal-link: + specifier: ^2.1.1 + version: 2.1.1 + devDependencies: + '@types/command-exists': + specifier: ^1.2.0 + version: 1.2.1 + '@types/degit': + specifier: ^2.8.3 + version: 2.8.4 + '@types/fs-extra': + specifier: ^9.0.12 + version: 9.0.13 + '@types/jest': + specifier: ^27.0.3 + version: 27.5.2 + '@types/node': + specifier: ^16.6.2 + version: 16.18.58 + '@types/prompts': + specifier: ^2.4.1 + version: 2.4.5 + ts-jest: + specifier: ^29.1.0 + version: 29.1.1(@babel/core@7.22.20)(jest@29.6.4)(typescript@5.2.2) + packages/db-mongodb: dependencies: bson-objectid: @@ -3482,7 +3546,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.6.2 + '@types/node': 16.18.58 '@types/yargs': 17.0.24 chalk: 4.1.2 @@ -3529,6 +3593,18 @@ packages: /@juggle/resize-observer@3.4.0: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + /@kwsites/file-exists@1.1.1: + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@kwsites/promise-deferred@1.1.1: + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + dev: true + /@lexical/clipboard@0.12.2(lexical@0.12.2): resolution: {integrity: sha512-RldmfZquuJJJCJ5WquCyoJ1/eZ+AnNgdksqvd+G+Yn/GyJl/+O3dnHM0QVaDSPvh/PynLFcCtz/57ySLo2kQxQ==} peerDependencies: @@ -4116,6 +4192,22 @@ packages: engines: {node: '>=14.16'} dev: true + /@sindresorhus/slugify@1.1.2: + resolution: {integrity: sha512-V9nR/W0Xd9TSGXpZ4iFUcFGhuOJtZX82Fzxj1YISlbSgKvIiNa7eLEZrT0vAraPOt++KHauIVNYgGRgjc13dXA==} + engines: {node: '>=10'} + dependencies: + '@sindresorhus/transliterate': 0.1.2 + escape-string-regexp: 4.0.0 + dev: false + + /@sindresorhus/transliterate@0.1.2: + resolution: {integrity: sha512-5/kmIOY9FF32nicXH+5yLNTX4NJ4atl7jRgqAJuIn/iyDFXBktOKDxCvyGE/EzmF4ngSUvjXxQUQlQiZ5lfw+w==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + lodash.deburr: 4.1.0 + dev: false + /@sinonjs/commons@3.0.0: resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} dependencies: @@ -5236,6 +5328,10 @@ packages: source-map: 0.6.1 dev: true + /@types/command-exists@1.2.1: + resolution: {integrity: sha512-N+I0Iho/m1c63+g3E7ZgfGnxGZIRAoj9TUU7j8NFCt+RowqpTLQanLdcfzrCyiHtdEI2MKF1vLMgdyE5rETSnw==} + dev: true + /@types/compression@1.7.2: resolution: {integrity: sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg==} dependencies: @@ -5248,6 +5344,10 @@ packages: '@types/node': 20.6.2 dev: true + /@types/degit@2.8.4: + resolution: {integrity: sha512-E9ZPeZwh81/gDPVH4XpvcS4ewH/Ub4XJeM5xYAUP0BexGORIyCRYzSivlGOuGbVc4MH3//+z3h4CbrnMZMeUdA==} + dev: true + /@types/eslint-scope@3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: @@ -5311,6 +5411,12 @@ packages: '@types/node': 20.6.2 dev: true + /@types/fs-extra@9.0.13: + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + dependencies: + '@types/node': 16.18.58 + dev: true + /@types/glob@7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: @@ -5383,6 +5489,13 @@ packages: dependencies: '@types/istanbul-lib-report': 3.0.0 + /@types/jest@27.5.2: + resolution: {integrity: sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==} + dependencies: + jest-matcher-utils: 27.5.1 + pretty-format: 27.5.1 + dev: true + /@types/jest@29.5.4: resolution: {integrity: sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==} dependencies: @@ -5491,6 +5604,9 @@ packages: '@types/node': 20.6.2 form-data: 3.0.1 + /@types/node@16.18.58: + resolution: {integrity: sha512-YGncyA25/MaVtQkjWW9r0EFBukZ+JulsLcVZBlGUfIb96OBMjkoRWwQo5IEWJ8Fj06Go3GHw+bjYDitv6BaGsA==} + /@types/node@18.17.17: resolution: {integrity: sha512-cOxcXsQ2sxiwkykdJqvyFS+MLQPLvIdwh5l6gNg8qF6s+C7XSkEWOZjK+XhUZd+mYvHV/180g2cnCcIl4l06Pw==} dev: false @@ -5558,7 +5674,7 @@ packages: /@types/pg@8.10.2: resolution: {integrity: sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==} dependencies: - '@types/node': 20.5.7 + '@types/node': 20.6.2 pg-protocol: 1.6.0 pg-types: 4.0.1 @@ -6420,6 +6536,10 @@ packages: /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: false + /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -6569,6 +6689,11 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + /at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: false + /atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -7150,7 +7275,6 @@ packages: engines: {node: '>=8'} dependencies: restore-cursor: 3.1.0 - dev: true /cli-cursor@4.0.0: resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} @@ -7162,7 +7286,6 @@ packages: /cli-spinners@2.9.1: resolution: {integrity: sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==} engines: {node: '>=6'} - dev: true /cli-truncate@3.1.0: resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} @@ -7210,7 +7333,6 @@ packages: /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - dev: true /clsx@1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} @@ -7265,6 +7387,10 @@ packages: dependencies: delayed-stream: 1.0.0 + /command-exists@1.2.9: + resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} + dev: false + /commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -7594,6 +7720,25 @@ packages: path-type: 4.0.0 dev: true + /create-jest@29.7.0(@types/node@16.18.58)(ts-node@10.9.1): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@16.18.58)(ts-node@10.9.1) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /create-jest@29.7.0(@types/node@20.5.7)(ts-node@10.9.1): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8050,7 +8195,7 @@ packages: dependencies: bundle-name: 3.0.0 default-browser-id: 3.0.0 - execa: 7.1.1 + execa: 7.2.0 titleize: 3.0.0 dev: true @@ -8058,7 +8203,6 @@ packages: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} dependencies: clone: 1.0.4 - dev: true /defer-to-connect@2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} @@ -8095,6 +8239,12 @@ packages: esprima: 4.0.1 dev: true + /degit@2.8.4: + resolution: {integrity: sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng==} + engines: {node: '>=8.0.0'} + hasBin: true + dev: false + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -8128,6 +8278,11 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + /diff-sequences@27.5.1: + resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dev: true + /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9301,6 +9456,13 @@ packages: node-domexception: 1.0.0 web-streams-polyfill: 3.2.1 + /figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 1.0.5 + dev: false + /figures@5.0.0: resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==} engines: {node: '>=14'} @@ -9557,6 +9719,16 @@ packages: universalify: 0.1.2 dev: true + /fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: false + /fs-monkey@1.0.4: resolution: {integrity: sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ==} @@ -9987,7 +10159,6 @@ packages: wordwrap: 1.0.0 optionalDependencies: uglify-js: 3.17.4 - dev: true /hanji@0.0.5: resolution: {integrity: sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==} @@ -10551,7 +10722,6 @@ packages: /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} - dev: true /is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} @@ -10677,7 +10847,6 @@ packages: /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - dev: true /is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} @@ -10878,6 +11047,34 @@ packages: - babel-plugin-macros - supports-color + /jest-cli@29.7.0(@types/node@16.18.58)(ts-node@10.9.1): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@16.18.58)(ts-node@10.9.1) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@16.18.58)(ts-node@10.9.1) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-cli@29.7.0(@types/node@20.5.7)(ts-node@10.9.1): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -10905,6 +11102,47 @@ packages: - supports-color - ts-node + /jest-config@29.7.0(@types/node@16.18.58)(ts-node@10.9.1): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.22.20 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 16.18.58 + babel-jest: 29.7.0(@babel/core@7.22.20) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.1(@swc/core@1.3.76)(@types/node@20.5.7)(typescript@5.2.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-config@29.7.0(@types/node@20.5.7)(ts-node@10.9.1): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -10985,6 +11223,16 @@ packages: - babel-plugin-macros - supports-color + /jest-diff@27.5.1: + resolution: {integrity: sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 27.5.1 + jest-get-type: 27.5.1 + pretty-format: 27.5.1 + dev: true + /jest-diff@29.7.0: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -11044,6 +11292,11 @@ packages: jest-mock: 29.7.0 jest-util: 29.7.0 + /jest-get-type@27.5.1: + resolution: {integrity: sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dev: true + /jest-get-type@29.6.3: resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -11073,6 +11326,16 @@ packages: jest-get-type: 29.6.3 pretty-format: 29.7.0 + /jest-matcher-utils@27.5.1: + resolution: {integrity: sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 27.5.1 + jest-get-type: 27.5.1 + pretty-format: 27.5.1 + dev: true + /jest-matcher-utils@29.7.0: resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -11231,7 +11494,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.6.2 + '@types/node': 16.18.58 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.11 @@ -11278,6 +11541,27 @@ packages: merge-stream: 2.0.0 supports-color: 8.1.1 + /jest@29.6.4(@types/node@16.18.58)(ts-node@10.9.1): + resolution: {integrity: sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@16.18.58)(ts-node@10.9.1) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest@29.6.4(@types/node@20.5.7)(ts-node@10.9.1): resolution: {integrity: sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -11707,6 +11991,10 @@ packages: /lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + /lodash.deburr@4.1.0: + resolution: {integrity: sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==} + dev: false + /lodash.escape@4.0.1: resolution: {integrity: sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==} @@ -11764,7 +12052,6 @@ packages: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 - dev: true /log-symbols@5.1.0: resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} @@ -12663,7 +12950,6 @@ packages: log-symbols: 4.1.0 strip-ansi: 6.0.1 wcwidth: 1.0.1 - dev: true /ora@6.3.1: resolution: {integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==} @@ -14850,7 +15136,6 @@ packages: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 - dev: true /restore-cursor@4.0.0: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} @@ -15208,6 +15493,16 @@ packages: once: 1.4.0 simple-concat: 1.0.1 + /simple-git@3.20.0: + resolution: {integrity: sha512-ozK8tl2hvLts8ijTs18iFruE+RoqmC/mqZhjs/+V7gS5W68JpJ3+FCTmLVqmR59MaUQ52MfGQuWsIqfsTbbJ0Q==} + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + /simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} dependencies: @@ -15661,6 +15956,14 @@ packages: dependencies: has-flag: 4.0.0 + /supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + dev: false + /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -15746,6 +16049,14 @@ packages: inherits: 2.0.4 readable-stream: 3.6.2 + /terminal-link@2.1.1: + resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} + engines: {node: '>=8'} + dependencies: + ansi-escapes: 4.3.2 + supports-hyperlinks: 2.3.0 + dev: false + /terser-webpack-plugin@5.3.9(@swc/core@1.3.76)(webpack@5.88.2): resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} engines: {node: '>= 10.13.0'} @@ -16012,7 +16323,7 @@ packages: '@babel/core': 7.22.20 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.6.4(@types/node@20.5.7)(ts-node@10.9.1) + jest: 29.6.4(@types/node@16.18.58)(ts-node@10.9.1) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -16272,7 +16583,6 @@ packages: engines: {node: '>=0.8.0'} hasBin: true requiresBuild: true - dev: true optional: true /unbox-primitive@1.0.2: @@ -16541,7 +16851,6 @@ packages: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} dependencies: defaults: 1.0.4 - dev: true /web-streams-polyfill@3.2.1: resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} diff --git a/scripts/list_published_packages.sh b/scripts/list_published_packages.sh deleted file mode 100755 index 55a78c9a7..000000000 --- a/scripts/list_published_packages.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -# List all published packages - -packages=$(find packages -name package.json -type f -exec grep -L '"private": true' {} \; | xargs jq -r '.name') - -# sort alphabetically -packages=$(echo "$packages" | tr ' ' '\n' | sort -u | tr '\n' ' ') - -# Loop through each package and print the name and version. Print as table - -printf "%-30s %-20s %-20s\n" "package" "latest" "beta" - -for package in $packages; do - info=$(npm view "$package" dist-tags --json) - latest=$(echo "$info" | jq -r '.latest') - beta=$(echo "$info" | jq -r '.beta') - printf "%-30s %-20s %-20s\n" "$package" "$latest" "$beta" -done diff --git a/scripts/publish.ts b/scripts/release.ts similarity index 100% rename from scripts/publish.ts rename to scripts/release.ts diff --git a/scripts/release_beta.sh b/scripts/release_beta.sh deleted file mode 100755 index a990dc91a..000000000 --- a/scripts/release_beta.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -# Build packages/payload - -package_name=$1 -package_dir="packages/$package_name" - -if [ -z "$package_name" ]; then - echo "Please specify a package to publish" - exit 1 -fi - -# Check if packages/$package_name exists - -if [ ! -d "$package_dir" ]; then - echo "Package $package_name does not exist" - exit 1 -fi - -npm --prefix "$package_dir" version pre --preid beta -git add "$package_dir"/package.json -new_version=$(node -p "require('./$package_dir/package.json').version") -git commit -m "chore(release): $package_name@$new_version" -pnpm publish -C "$package_dir" --tag beta --no-git-checks diff --git a/templates/blank/package.json b/templates/blank/package.json index 3969a500e..c0bcfeba4 100644 --- a/templates/blank/package.json +++ b/templates/blank/package.json @@ -15,14 +15,14 @@ "generate:graphQLSchema": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema" }, "dependencies": { - "@payloadcms/bundler-webpack": "latest", - "@payloadcms/db-mongodb": "latest", - "@payloadcms/plugin-cloud": "^0.0.10", - "@payloadcms/richtext-slate": "latest", + "@payloadcms/bundler-webpack": "^1.0.0", + "@payloadcms/db-mongodb": "^1.0.0", + "@payloadcms/plugin-cloud": "^2.0.0", + "@payloadcms/richtext-slate": "^1.0.0", "cross-env": "^7.0.3", "dotenv": "^8.2.0", "express": "^4.17.1", - "payload": "latest" + "payload": "^2.0.0" }, "devDependencies": { "@types/express": "^4.17.9", diff --git a/templates/ecommerce/package.json b/templates/ecommerce/package.json index ce728a75f..0e99d6826 100644 --- a/templates/ecommerce/package.json +++ b/templates/ecommerce/package.json @@ -21,32 +21,38 @@ "lint:fix": "eslint --fix --ext .ts,.tsx src" }, "dependencies": { - "@payloadcms/bundler-webpack": "^1.0.0-beta.6", - "@payloadcms/db-mongodb": "latest", + "@payloadcms/bundler-webpack": "^1.0.0", + "@payloadcms/db-mongodb": "^1.0.0", "@payloadcms/plugin-cloud": "^2.0.0", "@payloadcms/plugin-nested-docs": "^1.0.4", "@payloadcms/plugin-redirects": "^1.0.0", "@payloadcms/plugin-seo": "^1.0.10", "@payloadcms/plugin-stripe": "^0.0.14", - "@payloadcms/richtext-slate": "latest", + "@payloadcms/richtext-slate": "^1.0.0", "@stripe/react-stripe-js": "^1.16.3", "@stripe/stripe-js": "^1.46.0", "cross-env": "^7.0.3", "dotenv": "^8.2.0", + "escape-html": "^1.0.3", "express": "^4.17.1", + "qs": "6.11.2", "next": "^13.4.8", - "payload": "latest", + "payload": "^2.0.0", "payload-admin-bar": "^1.0.6", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.45.1", + "react-router-dom": "5.3.4", "stripe": "^10.2.0" }, "devDependencies": { "@next/eslint-plugin-next": "^13.1.6", "@payloadcms/eslint-config": "^0.0.1", + "@types/escape-html": "^1.0.2", "@types/express": "^4.17.9", + "@swc/core": "1.3.76", "@types/node": "18.11.3", + "@types/qs": "^6.9.8", "@types/react": "18.0.21", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", @@ -61,6 +67,7 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "nodemon": "^2.0.6", "prettier": "^2.7.1", + "slate": "0.91.4", "ts-node": "^10.9.1", "typescript": "^4.8.4" } diff --git a/templates/ecommerce/yarn.lock b/templates/ecommerce/yarn.lock index 74305b931..c999ba86e 100644 --- a/templates/ecommerce/yarn.lock +++ b/templates/ecommerce/yarn.lock @@ -1380,16 +1380,17 @@ resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323" integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== -"@payloadcms/bundler-webpack@^1.0.0-beta.6": - version "1.0.0-beta.6" - resolved "https://registry.npmjs.org/@payloadcms/bundler-webpack/-/bundler-webpack-1.0.0-beta.6.tgz#71a32de1b7569513cc29529eeedaec94643dfb96" - integrity sha512-wgCvr0iD2YU1GU5HgltFDDVzwEY9HCi+9rhEgVMHO9OFu/hBhfgG9eoMtgnzfoNs5FG9C2KIxJbTPlzwSqKfIA== +"@payloadcms/bundler-webpack@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@payloadcms/bundler-webpack/-/bundler-webpack-1.0.3.tgz#7126f5f7d1d3e7fba300e8e02082bbd681fe5ae5" + integrity sha512-zgcaEiDHxoJ4IxX/73rXY6nTiLy4/KjPt2ghjAGOh+Rht6Q6/CSJCcBcVvQGHaV8ynImPax7CHuYQKLNX5mWtQ== dependencies: compression "1.7.4" connect-history-api-fallback "1.6.0" css-loader "5.2.7" css-minimizer-webpack-plugin "^5.0.0" file-loader "6.2.0" + find-node-modules "^2.1.3" html-webpack-plugin "^5.5.0" md5 "2.3.0" mini-css-extract-plugin "1.6.2" @@ -1410,10 +1411,10 @@ webpack-dev-middleware "6.0.1" webpack-hot-middleware "^2.25.3" -"@payloadcms/db-mongodb@latest": - version "1.0.0-beta.9" - resolved "https://registry.yarnpkg.com/@payloadcms/db-mongodb/-/db-mongodb-1.0.0-beta.9.tgz#ee1570e25784bbe5fd11551309713b7ee69dcc0e" - integrity sha512-DUjBdbgZny0XTjWSBu70+C6XuyB53wA8KvEhcSM6Qcke2OBRsRvV+ON7ZlGkHyH6dio8eaB88o+hdKDYgm5unw== +"@payloadcms/db-mongodb@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@payloadcms/db-mongodb/-/db-mongodb-1.0.3.tgz#d106dbeb2c7d0c829927fbe8b8a3d5276204617f" + integrity sha512-9Zvyexg61Scdps5KIKVAM6ydRKL3moe0g2yiMBzdyDG0WuzAlI2xxz0P41hM6k402cSK42XOKj4Sqe6bghvr2g== dependencies: bson-objectid "2.0.4" deepmerge "4.3.1" @@ -1466,10 +1467,10 @@ stripe "^10.2.0" uuid "^9.0.0" -"@payloadcms/richtext-slate@latest": - version "1.0.0-beta.5" - resolved "https://registry.yarnpkg.com/@payloadcms/richtext-slate/-/richtext-slate-1.0.0-beta.5.tgz#669aaef59ff836f083cf177ccb55dac3f0aeecc4" - integrity sha512-+x8tC4iKf6evXVkIGf9Msb01ZgXr6hrLplDFKOWHzyHa+ScgsDQjfSVVUaK21b/LRFL3+DWhhanDHBC3Q+4/4A== +"@payloadcms/richtext-slate@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@payloadcms/richtext-slate/-/richtext-slate-1.0.2.tgz#bb3e00690f52aeb9d3e895c3e2c7c2dec6795fb6" + integrity sha512-TwOyYHmahfpmeRux7wtxA3k6juJqDmm3z/e6f1X7C9kIyWUR88T+YzMkFqt+Sm8pOjj7k0VNuV+uJMfy7opNmg== dependencies: "@faceless-ui/modal" "2.0.1" i18next "22.5.1" @@ -2097,6 +2098,11 @@ dependencies: "@types/node" "*" +"@types/escape-html@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.2.tgz#072b7b13784fb3cee9c2450c22f36405983f5e3c" + integrity sha512-gaBLT8pdcexFztLSPRtriHeXY/Kn4907uOCZ4Q3lncFBkheAWOuNt53ypsF8szgxbEJ513UeBzcf4utN0EzEwA== + "@types/eslint-scope@^3.7.3": version "3.7.5" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.5.tgz#e28b09dbb1d9d35fdfa8a884225f00440dfc5a3e" @@ -2240,7 +2246,7 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== -"@types/qs@*": +"@types/qs@*", "@types/qs@^6.9.8": version "6.9.8" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.8.tgz#f2a7de3c107b89b441e071d5472e6b726b4adf45" integrity sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg== @@ -3676,6 +3682,11 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q== + detect-libc@^2.0.0, detect-libc@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" @@ -4027,7 +4038,7 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== @@ -4244,6 +4255,13 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw== + dependencies: + homedir-polyfill "^1.0.1" + express-fileupload@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/express-fileupload/-/express-fileupload-1.4.0.tgz#be9d70a881d6c2b1ce668df86e4f89ddbf238ec7" @@ -4421,6 +4439,14 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +find-node-modules@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/find-node-modules/-/find-node-modules-2.1.3.tgz#3c976cff2ca29ee94b4f9eafc613987fc4c0ee44" + integrity sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg== + dependencies: + findup-sync "^4.0.0" + merge "^2.1.1" + find-root@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -4449,6 +4475,16 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +findup-sync@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-4.0.0.tgz#956c9cdde804052b881b428512905c4a5f2cdef0" + integrity sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^4.0.2" + resolve-dir "^1.0.1" + flat-cache@^3.0.4: version "3.1.0" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.0.tgz#0e54ab4a1a60fe87e2946b6b00657f1c99e1af3f" @@ -4668,6 +4704,26 @@ glob@^8.0.0, glob@^8.1.0: minimatch "^5.0.1" once "^1.3.0" +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg== + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + globals@^13.19.0: version "13.21.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.21.0.tgz#163aae12f34ef502f5153cfbdd3600f36c63c571" @@ -4839,6 +4895,13 @@ hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.1: dependencies: react-is "^16.7.0" +homedir-polyfill@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + html-entities@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061" @@ -5261,6 +5324,11 @@ is-whitespace@^0.3.0: resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f" integrity sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg== +is-windows@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -5806,6 +5874,11 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +merge@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98" + integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w== + method-override@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/method-override/-/method-override-3.0.0.tgz#6ab0d5d574e3208f15b0c9cf45ab52000468d7a2" @@ -5826,7 +5899,7 @@ micro-memoize@4.1.2: resolved "https://registry.yarnpkg.com/micro-memoize/-/micro-memoize-4.1.2.tgz#ce719c1ba1e41592f1cd91c64c5f41dcbf135f36" integrity sha512-+HzcV2H+rbSJzApgkj0NdTakkC+bnyeiUxgT6/m7mjcz1CmM22KYFKp+EVj1sWe4UYcnriJr5uqHQD/gMHLD+g== -micromatch@^4.0.4: +micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -6320,6 +6393,11 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q== + parseley@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.11.0.tgz#1ff817c829a02fcc214c9cc0d96b126d772ee814" @@ -6442,10 +6520,10 @@ payload-admin-bar@^1.0.6: resolved "https://registry.yarnpkg.com/payload-admin-bar/-/payload-admin-bar-1.0.6.tgz#ca25e369a4f319c3d61ff3db4021c1c2fd9d0148" integrity sha512-hpQdOiPq4LpWTkbuAnvxDf5wQ2ysMp9kQt+X2U+FfvBwD1U6qoxJfmUymG1OjLlaZzCZ93FlOdTl4u4Z0/m/SA== -payload@beta: - version "2.0.0-beta.30" - resolved "https://registry.yarnpkg.com/payload/-/payload-2.0.0-beta.30.tgz#56594994d70eef83ca95850a38dadad2c30e6494" - integrity sha512-haQ/LCJMNKWgimRlPs5HmNWbT2jP4SHYFxd2ht43hESZ4gE5uFvwsgpqeWwQiO+QCttmgNBkrfmJfNLLDm7hgw== +payload@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/payload/-/payload-2.0.4.tgz#e659f69d342789eab18dfe6d675218aad3796834" + integrity sha512-Eh75yw2xyDdir4Bfk3Gy6laJ4Q+FFvWkkRgGwJ1oDC60V6ztqMFLbejSdXNxLlfj800GYa9LwnXlFjUDV+2pmQ== dependencies: "@date-io/date-fns" "2.16.0" "@dnd-kit/core" "6.0.8" @@ -7649,6 +7727,14 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg== + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -8915,6 +9001,13 @@ which-typed-array@^1.1.10, which-typed-array@^1.1.11, which-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.0" +which@^1.2.14: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" diff --git a/templates/website/package.json b/templates/website/package.json index 7c1f986c4..010e0a22d 100644 --- a/templates/website/package.json +++ b/templates/website/package.json @@ -20,29 +20,35 @@ "lint:fix": "eslint --fix --ext .ts,.tsx src" }, "dependencies": { - "@payloadcms/bundler-webpack": "latest", - "@payloadcms/db-mongodb": "latest", + "@payloadcms/bundler-webpack": "^1.0.0", + "@payloadcms/db-mongodb": "^1.0.0", "@payloadcms/plugin-cloud": "^2.0.0", "@payloadcms/plugin-form-builder": "^1.0.13", "@payloadcms/plugin-nested-docs": "^1.0.4", "@payloadcms/plugin-redirects": "^1.0.0", "@payloadcms/plugin-seo": "^1.0.10", - "@payloadcms/richtext-slate": "latest", + "@payloadcms/richtext-slate": "^1.0.0", "cross-env": "^7.0.3", "dotenv": "^8.2.0", + "escape-html": "^1.0.3", "express": "^4.17.1", "next": "^13.4.8", - "payload": "latest", + "payload": "^2.0.0", "payload-admin-bar": "^1.0.6", + "qs": "6.11.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-hook-form": "^7.45.1" + "react-hook-form": "^7.45.1", + "react-router-dom": "5.3.4" }, "devDependencies": { "@next/eslint-plugin-next": "^13.1.6", "@payloadcms/eslint-config": "^0.0.1", + "@swc/core": "1.3.76", + "@types/escape-html": "^1.0.2", "@types/express": "^4.17.9", "@types/node": "18.11.3", + "@types/qs": "^6.9.8", "@types/react": "18.0.21", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", @@ -57,6 +63,7 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "nodemon": "^2.0.6", "prettier": "^2.7.1", + "slate": "0.91.4", "ts-node": "^10.9.1", "typescript": "^4.8.4" } diff --git a/templates/website/src/payload/components/BeforeDashboard/index.tsx b/templates/website/src/payload/components/BeforeDashboard/index.tsx index d559d6666..c7900afda 100644 --- a/templates/website/src/payload/components/BeforeDashboard/index.tsx +++ b/templates/website/src/payload/components/BeforeDashboard/index.tsx @@ -1,5 +1,4 @@ import React from 'react' -import { Link } from 'react-router-dom' import { Banner } from 'payload/components' import { SeedButton } from './SeedButton' diff --git a/templates/website/yarn.lock b/templates/website/yarn.lock index d9d4f8f66..f84e4568a 100644 --- a/templates/website/yarn.lock +++ b/templates/website/yarn.lock @@ -1431,16 +1431,17 @@ resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323" integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== -"@payloadcms/bundler-webpack@latest": - version "1.0.0-beta.6" - resolved "https://registry.yarnpkg.com/@payloadcms/bundler-webpack/-/bundler-webpack-1.0.0-beta.6.tgz#71a32de1b7569513cc29529eeedaec94643dfb96" - integrity sha512-wgCvr0iD2YU1GU5HgltFDDVzwEY9HCi+9rhEgVMHO9OFu/hBhfgG9eoMtgnzfoNs5FG9C2KIxJbTPlzwSqKfIA== +"@payloadcms/bundler-webpack@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@payloadcms/bundler-webpack/-/bundler-webpack-1.0.3.tgz#7126f5f7d1d3e7fba300e8e02082bbd681fe5ae5" + integrity sha512-zgcaEiDHxoJ4IxX/73rXY6nTiLy4/KjPt2ghjAGOh+Rht6Q6/CSJCcBcVvQGHaV8ynImPax7CHuYQKLNX5mWtQ== dependencies: compression "1.7.4" connect-history-api-fallback "1.6.0" css-loader "5.2.7" css-minimizer-webpack-plugin "^5.0.0" file-loader "6.2.0" + find-node-modules "^2.1.3" html-webpack-plugin "^5.5.0" md5 "2.3.0" mini-css-extract-plugin "1.6.2" @@ -1461,10 +1462,10 @@ webpack-dev-middleware "6.0.1" webpack-hot-middleware "^2.25.3" -"@payloadcms/db-mongodb@latest": - version "1.0.0-beta.9" - resolved "https://registry.yarnpkg.com/@payloadcms/db-mongodb/-/db-mongodb-1.0.0-beta.9.tgz#ee1570e25784bbe5fd11551309713b7ee69dcc0e" - integrity sha512-DUjBdbgZny0XTjWSBu70+C6XuyB53wA8KvEhcSM6Qcke2OBRsRvV+ON7ZlGkHyH6dio8eaB88o+hdKDYgm5unw== +"@payloadcms/db-mongodb@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@payloadcms/db-mongodb/-/db-mongodb-1.0.3.tgz#d106dbeb2c7d0c829927fbe8b8a3d5276204617f" + integrity sha512-9Zvyexg61Scdps5KIKVAM6ydRKL3moe0g2yiMBzdyDG0WuzAlI2xxz0P41hM6k402cSK42XOKj4Sqe6bghvr2g== dependencies: bson-objectid "2.0.4" deepmerge "4.3.1" @@ -1515,10 +1516,10 @@ resolved "https://registry.yarnpkg.com/@payloadcms/plugin-seo/-/plugin-seo-1.0.13.tgz#b1d2f24f5cabf14e8ab7db6815b6fb41ee718557" integrity sha512-GPIyGTe6vU8GRFMYhNoUDrGWxm0uiqfIQQSsAuZjEM6YGFozo/AnpnjcT6VS/led/7HRFF8ijOy82l0VaLNC3w== -"@payloadcms/richtext-slate@latest": - version "1.0.0-beta.5" - resolved "https://registry.yarnpkg.com/@payloadcms/richtext-slate/-/richtext-slate-1.0.0-beta.5.tgz#669aaef59ff836f083cf177ccb55dac3f0aeecc4" - integrity sha512-+x8tC4iKf6evXVkIGf9Msb01ZgXr6hrLplDFKOWHzyHa+ScgsDQjfSVVUaK21b/LRFL3+DWhhanDHBC3Q+4/4A== +"@payloadcms/richtext-slate@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@payloadcms/richtext-slate/-/richtext-slate-1.0.2.tgz#bb3e00690f52aeb9d3e895c3e2c7c2dec6795fb6" + integrity sha512-TwOyYHmahfpmeRux7wtxA3k6juJqDmm3z/e6f1X7C9kIyWUR88T+YzMkFqt+Sm8pOjj7k0VNuV+uJMfy7opNmg== dependencies: "@faceless-ui/modal" "2.0.1" i18next "22.5.1" @@ -2134,6 +2135,11 @@ dependencies: "@types/node" "*" +"@types/escape-html@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.2.tgz#072b7b13784fb3cee9c2450c22f36405983f5e3c" + integrity sha512-gaBLT8pdcexFztLSPRtriHeXY/Kn4907uOCZ4Q3lncFBkheAWOuNt53ypsF8szgxbEJ513UeBzcf4utN0EzEwA== + "@types/eslint-scope@^3.7.3": version "3.7.4" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" @@ -2277,6 +2283,11 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== +"@types/qs@^6.9.8": + version "6.9.8" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.8.tgz#f2a7de3c107b89b441e071d5472e6b726b4adf45" + integrity sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg== + "@types/range-parser@*": version "1.2.4" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" @@ -3702,6 +3713,11 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q== + detect-libc@^2.0.0, detect-libc@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" @@ -4270,6 +4286,13 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw== + dependencies: + homedir-polyfill "^1.0.1" + express-fileupload@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/express-fileupload/-/express-fileupload-1.4.0.tgz#be9d70a881d6c2b1ce668df86e4f89ddbf238ec7" @@ -4447,6 +4470,14 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +find-node-modules@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/find-node-modules/-/find-node-modules-2.1.3.tgz#3c976cff2ca29ee94b4f9eafc613987fc4c0ee44" + integrity sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg== + dependencies: + findup-sync "^4.0.0" + merge "^2.1.1" + find-root@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -4475,6 +4506,16 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +findup-sync@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-4.0.0.tgz#956c9cdde804052b881b428512905c4a5f2cdef0" + integrity sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^4.0.2" + resolve-dir "^1.0.1" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -4693,6 +4734,26 @@ glob@^8.0.0, glob@^8.1.0: minimatch "^5.0.1" once "^1.3.0" +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg== + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + globals@^13.19.0: version "13.20.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" @@ -4864,6 +4925,13 @@ hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.1: dependencies: react-is "^16.7.0" +homedir-polyfill@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + html-entities@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061" @@ -5286,6 +5354,11 @@ is-whitespace@^0.3.0: resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f" integrity sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg== +is-windows@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -5738,6 +5811,11 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +merge@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98" + integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w== + method-override@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/method-override/-/method-override-3.0.0.tgz#6ab0d5d574e3208f15b0c9cf45ab52000468d7a2" @@ -5758,7 +5836,7 @@ micro-memoize@4.1.2: resolved "https://registry.yarnpkg.com/micro-memoize/-/micro-memoize-4.1.2.tgz#ce719c1ba1e41592f1cd91c64c5f41dcbf135f36" integrity sha512-+HzcV2H+rbSJzApgkj0NdTakkC+bnyeiUxgT6/m7mjcz1CmM22KYFKp+EVj1sWe4UYcnriJr5uqHQD/gMHLD+g== -micromatch@^4.0.4: +micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -6252,6 +6330,11 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q== + parseley@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.11.0.tgz#1ff817c829a02fcc214c9cc0d96b126d772ee814" @@ -6374,10 +6457,10 @@ payload-admin-bar@^1.0.6: resolved "https://registry.yarnpkg.com/payload-admin-bar/-/payload-admin-bar-1.0.6.tgz#ca25e369a4f319c3d61ff3db4021c1c2fd9d0148" integrity sha512-hpQdOiPq4LpWTkbuAnvxDf5wQ2ysMp9kQt+X2U+FfvBwD1U6qoxJfmUymG1OjLlaZzCZ93FlOdTl4u4Z0/m/SA== -payload@^2.0.0-beta.31: - version "2.0.0-beta.31" - resolved "https://registry.npmjs.org/payload/-/payload-2.0.0-beta.31.tgz#e67390378dacdcbd0fb47a23fe29b1d1110de37c" - integrity sha512-QJqEBSzmYW/ZxMRNWk+oHPMBPUuP0q8RBD/a+ufg1V0UGilkdce6F1Jfx5i0R7xSfUd2sGK3cU9KZ3L8/0WlCg== +payload@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/payload/-/payload-2.0.4.tgz#e659f69d342789eab18dfe6d675218aad3796834" + integrity sha512-Eh75yw2xyDdir4Bfk3Gy6laJ4Q+FFvWkkRgGwJ1oDC60V6ztqMFLbejSdXNxLlfj800GYa9LwnXlFjUDV+2pmQ== dependencies: "@date-io/date-fns" "2.16.0" "@dnd-kit/core" "6.0.8" @@ -7582,6 +7665,14 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg== + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -8829,6 +8920,13 @@ which-typed-array@^1.1.10, which-typed-array@^1.1.11, which-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.0" +which@^1.2.14: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" diff --git a/test/admin/collections/CustomViews1.ts b/test/admin/collections/CustomViews1.ts new file mode 100644 index 000000000..5b230088a --- /dev/null +++ b/test/admin/collections/CustomViews1.ts @@ -0,0 +1,23 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +import CustomEditView from '../components/views/CustomEdit' + +export const CustomViews1: CollectionConfig = { + slug: 'custom-views-one', + versions: true, + admin: { + components: { + views: { + // This will override the entire Edit view including all nested views, i.e. `/edit/:id/*` + // To override one specific nested view, use the nested view's slug as the key + Edit: CustomEditView, + }, + }, + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/collections/CustomViews2.ts b/test/admin/collections/CustomViews2.ts new file mode 100644 index 000000000..7d6d7863d --- /dev/null +++ b/test/admin/collections/CustomViews2.ts @@ -0,0 +1,41 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +import CustomTabComponent from '../components/CustomTabComponent' +import CustomDefaultEditView from '../components/views/CustomDefaultEdit' +import CustomVersionsView from '../components/views/CustomVersions' +import CustomView from '../components/views/CustomView' + +export const CustomViews2: CollectionConfig = { + slug: 'custom-views-two', + versions: true, + admin: { + components: { + views: { + Edit: { + // This will override one specific nested view within the `/edit/:id` route, i.e. `/edit/:id/versions` + Default: CustomDefaultEditView, + Versions: CustomVersionsView, + MyCustomView: { + path: '/custom-tab-view', + Component: CustomView, + Tab: { + label: 'Custom', + href: '/custom-tab-view', + }, + }, + MyCustomViewWithCustomTab: { + path: '/custom-tab-component', + Component: CustomView, + Tab: CustomTabComponent, + }, + }, + }, + }, + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/collections/Geo.ts b/test/admin/collections/Geo.ts new file mode 100644 index 000000000..14029e6a3 --- /dev/null +++ b/test/admin/collections/Geo.ts @@ -0,0 +1,11 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +export const Geo: CollectionConfig = { + slug: 'geo', + fields: [ + { + name: 'point', + type: 'point', + }, + ], +} diff --git a/test/admin/collections/Group1A.ts b/test/admin/collections/Group1A.ts new file mode 100644 index 000000000..0cd55d9a3 --- /dev/null +++ b/test/admin/collections/Group1A.ts @@ -0,0 +1,16 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +import { group1Collection1Slug } from '../shared' + +export const CollectionGroup1A: CollectionConfig = { + slug: group1Collection1Slug, + admin: { + group: 'One', + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/collections/Group1B.ts b/test/admin/collections/Group1B.ts new file mode 100644 index 000000000..7334d6014 --- /dev/null +++ b/test/admin/collections/Group1B.ts @@ -0,0 +1,16 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +import { group1Collection2Slug } from '../shared' + +export const CollectionGroup1B: CollectionConfig = { + slug: group1Collection2Slug, + admin: { + group: 'One', + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/collections/Group2A.ts b/test/admin/collections/Group2A.ts new file mode 100644 index 000000000..444480761 --- /dev/null +++ b/test/admin/collections/Group2A.ts @@ -0,0 +1,14 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +export const CollectionGroup2A: CollectionConfig = { + slug: 'group-two-collection-ones', + admin: { + group: 'One', + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/collections/Group2B.ts b/test/admin/collections/Group2B.ts new file mode 100644 index 000000000..bcb9928fb --- /dev/null +++ b/test/admin/collections/Group2B.ts @@ -0,0 +1,14 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +export const CollectionGroup2B: CollectionConfig = { + slug: 'group-two-collection-twos', + admin: { + group: 'One', + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/collections/Hidden.ts b/test/admin/collections/Hidden.ts new file mode 100644 index 000000000..0da1f7c94 --- /dev/null +++ b/test/admin/collections/Hidden.ts @@ -0,0 +1,14 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +export const CollectionHidden: CollectionConfig = { + slug: 'hidden-collection', + admin: { + hidden: () => true, + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/collections/Posts.ts b/test/admin/collections/Posts.ts new file mode 100644 index 000000000..1484616b2 --- /dev/null +++ b/test/admin/collections/Posts.ts @@ -0,0 +1,96 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +import { slateEditor } from '../../../packages/richtext-slate/src' +import DemoUIFieldCell from '../components/DemoUIField/Cell' +import DemoUIFieldField from '../components/DemoUIField/Field' +import { postsSlug, slugPluralLabel, slugSingularLabel } from '../shared' + +export const Posts: CollectionConfig = { + slug: postsSlug, + labels: { + singular: slugSingularLabel, + plural: slugPluralLabel, + }, + admin: { + description: 'Description', + listSearchableFields: ['title', 'description', 'number'], + group: 'One', + useAsTitle: 'title', + defaultColumns: ['id', 'number', 'title', 'description', 'demoUIField'], + preview: () => 'https://payloadcms.com', + }, + versions: { + drafts: true, + }, + fields: [ + { + type: 'tabs', + tabs: [ + { + label: 'Tab 1', + fields: [ + { + name: 'title', + type: 'text', + }, + { + name: 'description', + type: 'text', + }, + { + name: 'number', + type: 'number', + }, + { + name: 'richText', + type: 'richText', + editor: slateEditor({ + admin: { + elements: ['relationship'], + }, + }), + }, + { + type: 'ui', + name: 'demoUIField', + label: 'Demo UI Field', + admin: { + components: { + Field: DemoUIFieldField, + Cell: DemoUIFieldCell, + }, + }, + }, + ], + }, + ], + }, + { + name: 'group', + type: 'group', + fields: [ + { + name: 'title', + type: 'text', + }, + ], + }, + { + name: 'relationship', + type: 'relationship', + relationTo: 'posts', + admin: { + position: 'sidebar', + }, + }, + { + name: 'sidebarField', + type: 'text', + admin: { + position: 'sidebar', + description: + 'This is a very long description that takes many characters to complete and hopefully will wrap instead of push the sidebar open, lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum voluptates. Quisquam, voluptatum voluptates.', + }, + }, + ], +} diff --git a/test/admin/collections/Users.ts b/test/admin/collections/Users.ts new file mode 100644 index 000000000..9469fdbda --- /dev/null +++ b/test/admin/collections/Users.ts @@ -0,0 +1,10 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +export const Users: CollectionConfig = { + slug: 'users', + auth: true, + admin: { + useAsTitle: 'email', + }, + fields: [], +} diff --git a/test/admin/components/views/CustomDefaultEdit/index.tsx b/test/admin/components/views/CustomDefaultEdit/index.tsx index 05d0548ba..3d94bf93f 100644 --- a/test/admin/components/views/CustomDefaultEdit/index.tsx +++ b/test/admin/components/views/CustomDefaultEdit/index.tsx @@ -6,7 +6,7 @@ import type { AdminViewComponent } from '../../../../../packages/payload/src/con import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav' import { useConfig } from '../../../../../packages/payload/src/admin/components/utilities/Config' -const CustomDefaultView: AdminViewComponent = ({ +const CustomDefaultEditView: AdminViewComponent = ({ canAccessAdmin, // collection, // global, @@ -72,4 +72,4 @@ const CustomDefaultView: AdminViewComponent = ({ ) } -export default CustomDefaultView +export default CustomDefaultEditView diff --git a/test/admin/config.ts b/test/admin/config.ts index db03212d1..6fdc7ca9d 100644 --- a/test/admin/config.ts +++ b/test/admin/config.ts @@ -1,23 +1,31 @@ import path from 'path' import { mapAsync } from '../../packages/payload/src/utilities/mapAsync' -import { slateEditor } from '../../packages/richtext-slate/src' import { buildConfigWithDefaults } from '../buildConfigWithDefaults' import { devUser } from '../credentials' +import { CustomViews1 } from './collections/CustomViews1' +import { CustomViews2 } from './collections/CustomViews2' +import { Geo } from './collections/Geo' +import { CollectionGroup1A } from './collections/Group1A' +import { CollectionGroup1B } from './collections/Group1B' +import { CollectionGroup2A } from './collections/Group2A' +import { CollectionGroup2B } from './collections/Group2B' +import { CollectionHidden } from './collections/Hidden' +import { Posts } from './collections/Posts' +import { Users } from './collections/Users' import AfterDashboard from './components/AfterDashboard' import AfterNavLinks from './components/AfterNavLinks' import BeforeLogin from './components/BeforeLogin' -import CustomTabComponent from './components/CustomTabComponent' -import DemoUIFieldCell from './components/DemoUIField/Cell' -import DemoUIFieldField from './components/DemoUIField/Field' import Logout from './components/Logout' import CustomDefaultView from './components/views/CustomDefault' -import CustomDefaultEditView from './components/views/CustomDefaultEdit' -import CustomEditView from './components/views/CustomEdit' import CustomMinimalRoute from './components/views/CustomMinimal' -import CustomVersionsView from './components/views/CustomVersions' -import CustomView from './components/views/CustomView' -import { globalSlug, postsSlug, slugPluralLabel, slugSingularLabel } from './shared' +import { CustomGlobalViews1 } from './globals/CustomViews1' +import { CustomGlobalViews2 } from './globals/CustomViews2' +import { Global } from './globals/Global' +import { GlobalGroup1A } from './globals/Group1A' +import { GlobalGroup1B } from './globals/Group1B' +import { GlobalHidden } from './globals/Hidden' +import { postsSlug } from './shared' export interface Post { createdAt: Date @@ -66,321 +74,24 @@ export default buildConfigWithDefaults({ locales: ['en', 'es'], }, collections: [ - { - slug: 'users', - auth: true, - admin: { - useAsTitle: 'email', - }, - fields: [], - }, - { - slug: 'hidden-collection', - admin: { - hidden: () => true, - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, - { - slug: postsSlug, - labels: { - singular: slugSingularLabel, - plural: slugPluralLabel, - }, - admin: { - description: 'Description', - listSearchableFields: ['title', 'description', 'number'], - group: 'One', - useAsTitle: 'title', - defaultColumns: ['id', 'number', 'title', 'description', 'demoUIField'], - preview: () => 'https://payloadcms.com', - }, - versions: { - drafts: true, - }, - fields: [ - { - type: 'tabs', - tabs: [ - { - label: 'Tab 1', - fields: [ - { - name: 'title', - type: 'text', - }, - { - name: 'description', - type: 'text', - }, - { - name: 'number', - type: 'number', - }, - { - name: 'richText', - type: 'richText', - editor: slateEditor({ - admin: { - elements: ['relationship'], - }, - }), - }, - { - type: 'ui', - name: 'demoUIField', - label: 'Demo UI Field', - admin: { - components: { - Field: DemoUIFieldField, - Cell: DemoUIFieldCell, - }, - }, - }, - ], - }, - ], - }, - { - name: 'sidebarField', - type: 'text', - admin: { - position: 'sidebar', - description: - 'This is a very long description that takes many characters to complete and hopefully will wrap instead of push the sidebar open, lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum voluptates. Quisquam, voluptatum voluptates.', - }, - }, - ], - }, - { - slug: 'custom-views-one', - versions: true, - admin: { - components: { - views: { - // This will override the entire Edit view including all nested views, i.e. `/edit/:id/*` - // To override one specific nested view, use the nested view's slug as the key - Edit: CustomEditView, - }, - }, - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, - { - slug: 'custom-views-two', - versions: true, - admin: { - components: { - views: { - Edit: { - // This will override one specific nested view within the `/edit/:id` route, i.e. `/edit/:id/versions` - Default: CustomDefaultEditView, - Versions: CustomVersionsView, - MyCustomView: { - path: '/custom-tab-view', - Component: CustomView, - Tab: { - label: 'Custom', - href: '/custom-tab-view', - }, - }, - MyCustomViewWithCustomTab: { - path: '/custom-tab-component', - Component: CustomView, - Tab: CustomTabComponent, - }, - }, - }, - }, - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, - { - slug: 'group-one-collection-ones', - admin: { - group: 'One', - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, - { - slug: 'group-one-collection-twos', - admin: { - group: 'One', - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, - { - slug: 'group-two-collection-ones', - admin: { - group: 'Two', - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, - { - slug: 'group-two-collection-twos', - admin: { - group: 'Two', - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, - { - slug: 'geo', - fields: [ - { - name: 'point', - type: 'point', - }, - ], - }, + Posts, + Users, + CollectionHidden, + CustomViews1, + CustomViews2, + CollectionGroup1A, + CollectionGroup1B, + CollectionGroup2A, + CollectionGroup2B, + Geo, ], globals: [ - { - slug: 'hidden-global', - admin: { - hidden: () => true, - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, - { - slug: globalSlug, - label: { - en: 'My Global Label', - }, - admin: { - group: 'Group', - }, - versions: { - drafts: true, - }, - fields: [ - { - name: 'title', - type: 'text', - }, - { - name: 'sidebarField', - type: 'text', - admin: { - position: 'sidebar', - }, - }, - ], - }, - { - slug: 'custom-global-views-one', - versions: true, - admin: { - components: { - views: { - Edit: CustomEditView, - }, - }, - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, - { - slug: 'custom-global-views-two', - versions: true, - admin: { - components: { - views: { - Edit: { - Default: CustomDefaultEditView, - Versions: CustomVersionsView, - MyCustomView: { - path: '/custom-tab-view', - Component: CustomView, - Tab: { - label: 'Custom', - href: '/custom-tab-view', - }, - }, - MyCustomViewWithCustomTab: { - path: '/custom-tab-component', - Component: CustomView, - Tab: CustomTabComponent, - }, - }, - }, - }, - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, - { - slug: 'group-globals-one', - label: 'Group Globals 1', - admin: { - group: 'Group', - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, - { - slug: 'group-globals-two', - admin: { - group: 'Group', - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], - }, + GlobalHidden, + Global, + CustomGlobalViews1, + CustomGlobalViews2, + GlobalGroup1A, + GlobalGroup1B, ], onInit: async (payload) => { await payload.create({ diff --git a/test/admin/e2e.spec.ts b/test/admin/e2e.spec.ts index 3682d6433..53c613f9b 100644 --- a/test/admin/e2e.spec.ts +++ b/test/admin/e2e.spec.ts @@ -17,7 +17,13 @@ import { } from '../helpers' import { AdminUrlUtil } from '../helpers/adminUrlUtil' import { initPayloadE2E } from '../helpers/configHelpers' -import { globalSlug, postsSlug, slugPluralLabel } from './shared' +import { + globalSlug, + group1Collection1Slug, + group1GlobalSlug, + postsSlug, + slugPluralLabel, +} from './shared' const { afterEach, beforeAll, beforeEach, describe } = test @@ -149,6 +155,38 @@ describe('admin', () => { }) }) + describe('ui', () => { + test('collection - should render preview button when `admin.preview` is set', async () => { + const collectionWithPreview = new AdminUrlUtil(serverURL, postsSlug) + await page.goto(collectionWithPreview.create) + await page.locator('#field-title').fill(title) + await saveDocAndAssert(page) + await expect(page.locator('.btn.preview-btn')).toBeVisible() + }) + + test('collection - should not render preview button when `admin.preview` is not set', async () => { + const collectionWithoutPreview = new AdminUrlUtil(serverURL, group1Collection1Slug) + await page.goto(collectionWithoutPreview.create) + await page.locator('#field-title').fill(title) + await saveDocAndAssert(page) + await expect(page.locator('.btn.preview-btn')).toBeHidden() + }) + + test('global - should render preview button when `admin.preview` is set', async () => { + const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug) + await page.goto(globalWithPreview.global(globalSlug)) + await expect(page.locator('.btn.preview-btn')).toBeVisible() + }) + + test('global - should not render preview button when `admin.preview` is not set', async () => { + const globalWithoutPreview = new AdminUrlUtil(serverURL, group1GlobalSlug) + await page.goto(globalWithoutPreview.global(group1GlobalSlug)) + await page.locator('#field-title').fill(title) + await saveDocAndAssert(page) + await expect(page.locator('.btn.preview-btn')).toBeHidden() + }) + }) + describe('doc titles', () => { test('collection - should render fallback titles when creating new', async () => { await page.goto(url.create) @@ -292,10 +330,12 @@ describe('admin', () => { await page.locator('input#select-all').check() await page.locator('.edit-many__toggle').click() await page.locator('.field-select .rs__control').click() - const options = page.locator('.rs__option') - const titleOption = options.locator('text=Title') - await expect(titleOption).toHaveText('Title') + const titleOption = page.locator('.rs__option', { + hasText: exactText('Title'), + }) + + await expect(titleOption).toBeVisible() await titleOption.click() const titleInput = page.locator('#field-title') diff --git a/test/admin/globals/CustomViews1.ts b/test/admin/globals/CustomViews1.ts new file mode 100644 index 000000000..ddfcc9a73 --- /dev/null +++ b/test/admin/globals/CustomViews1.ts @@ -0,0 +1,21 @@ +import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' + +import CustomEditView from '../components/views/CustomEdit' + +export const CustomGlobalViews1: GlobalConfig = { + slug: 'custom-global-views-one', + versions: true, + admin: { + components: { + views: { + Edit: CustomEditView, + }, + }, + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/globals/CustomViews2.ts b/test/admin/globals/CustomViews2.ts new file mode 100644 index 000000000..dcb95d264 --- /dev/null +++ b/test/admin/globals/CustomViews2.ts @@ -0,0 +1,40 @@ +import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' + +import CustomTabComponent from '../components/CustomTabComponent' +import CustomDefaultEditView from '../components/views/CustomDefaultEdit' +import CustomVersionsView from '../components/views/CustomVersions' +import CustomView from '../components/views/CustomView' + +export const CustomGlobalViews2: GlobalConfig = { + slug: 'custom-global-views-two', + versions: true, + admin: { + components: { + views: { + Edit: { + Default: CustomDefaultEditView, + Versions: CustomVersionsView, + MyCustomView: { + path: '/custom-tab-view', + Component: CustomView, + Tab: { + label: 'Custom', + href: '/custom-tab-view', + }, + }, + MyCustomViewWithCustomTab: { + path: '/custom-tab-component', + Component: CustomView, + Tab: CustomTabComponent, + }, + }, + }, + }, + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/globals/Global.ts b/test/admin/globals/Global.ts new file mode 100644 index 000000000..20f984b3b --- /dev/null +++ b/test/admin/globals/Global.ts @@ -0,0 +1,30 @@ +import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' + +import { globalSlug } from '../shared' + +export const Global: GlobalConfig = { + slug: globalSlug, + label: { + en: 'My Global Label', + }, + admin: { + group: 'Group', + preview: () => 'https://payloadcms.com', + }, + versions: { + drafts: true, + }, + fields: [ + { + name: 'title', + type: 'text', + }, + { + name: 'sidebarField', + type: 'text', + admin: { + position: 'sidebar', + }, + }, + ], +} diff --git a/test/admin/globals/Group1A.ts b/test/admin/globals/Group1A.ts new file mode 100644 index 000000000..0681ac156 --- /dev/null +++ b/test/admin/globals/Group1A.ts @@ -0,0 +1,17 @@ +import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' + +import { group1GlobalSlug } from '../shared' + +export const GlobalGroup1A: GlobalConfig = { + slug: group1GlobalSlug, + label: 'Group Globals 1', + admin: { + group: 'Group', + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/globals/Group1B.ts b/test/admin/globals/Group1B.ts new file mode 100644 index 000000000..861091ee8 --- /dev/null +++ b/test/admin/globals/Group1B.ts @@ -0,0 +1,14 @@ +import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' + +export const GlobalGroup1B: GlobalConfig = { + slug: 'group-globals-two', + admin: { + group: 'Group', + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/globals/Hidden.ts b/test/admin/globals/Hidden.ts new file mode 100644 index 000000000..fed2d8caa --- /dev/null +++ b/test/admin/globals/Hidden.ts @@ -0,0 +1,14 @@ +import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' + +export const GlobalHidden: GlobalConfig = { + slug: 'hidden-global', + admin: { + hidden: () => true, + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/shared.ts b/test/admin/shared.ts index bd1aaeee9..00a7fcaaf 100644 --- a/test/admin/shared.ts +++ b/test/admin/shared.ts @@ -1,7 +1,13 @@ export const postsSlug = 'posts' +export const group1Collection1Slug = 'group-one-collection-ones' + +export const group1Collection2Slug = 'group-one-collection-twos' + export const slugSingularLabel = 'Post' export const slugPluralLabel = 'Posts' export const globalSlug = 'global' + +export const group1GlobalSlug = 'group-globals-one' diff --git a/test/collections-rest/int.spec.ts b/test/collections-rest/int.spec.ts index 59c129128..c39b68c75 100644 --- a/test/collections-rest/int.spec.ts +++ b/test/collections-rest/int.spec.ts @@ -7,7 +7,14 @@ import payload from '../../packages/payload/src' import { mapAsync } from '../../packages/payload/src/utilities/mapAsync' import { initPayloadTest } from '../helpers/configHelpers' import { RESTClient } from '../helpers/rest' -import config, { customIdNumberSlug, customIdSlug, errorOnHookSlug, pointSlug, relationSlug, slug, } from './config' +import config, { + customIdNumberSlug, + customIdSlug, + errorOnHookSlug, + pointSlug, + relationSlug, + slug, +} from './config' let client: RESTClient @@ -676,6 +683,72 @@ describe('collections-rest', () => { expect(result.totalDocs).toEqual(1) }) + it('not_in (relationships)', async () => { + const relationship = await payload.create({ + collection: relationSlug, + data: {}, + }) + + await createPost({ relationField: relationship.id, title: 'not-me' }) + // await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' }) + const post2 = await createPost({ title: 'me' }) + const { result, status } = await client.find({ + query: { + relationField: { + not_in: [relationship.id], + }, + }, + }) + + // do not want to error for empty arrays + const { status: emptyNotInStatus } = await client.find({ + query: { + relationField: { + not_in: [], + }, + }, + }) + + expect(emptyNotInStatus).toEqual(200) + + expect(status).toEqual(200) + expect(result.docs).toEqual([post2]) + expect(result.totalDocs).toEqual(1) + }) + + it('in (relationships)', async () => { + const relationship = await payload.create({ + collection: relationSlug, + data: {}, + }) + + const post1 = await createPost({ relationField: relationship.id, title: 'me' }) + // await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' }) + await createPost({ title: 'not-me' }) + const { result, status } = await client.find({ + query: { + relationField: { + in: [relationship.id], + }, + }, + }) + + // do not want to error for empty arrays + const { status: emptyNotInStatus } = await client.find({ + query: { + relationField: { + in: [], + }, + }, + }) + + expect(emptyNotInStatus).toEqual(200) + + expect(status).toEqual(200) + expect(result.docs).toEqual([post1]) + expect(result.totalDocs).toEqual(1) + }) + it('like', async () => { const post1 = await createPost({ title: 'prefix-value' }) @@ -1175,18 +1248,18 @@ describe('collections-rest', () => { }) }) -async function createPost (overrides?: Partial) { +async function createPost(overrides?: Partial) { const { doc } = await client.create({ data: { title: 'title', ...overrides } }) return doc } -async function createPosts (count: number) { +async function createPosts(count: number) { await mapAsync([...Array(count)], async () => { await createPost() }) } -async function clearDocs (): Promise { +async function clearDocs(): Promise { const allDocs = await payload.find({ collection: slug, limit: 100 }) const ids = allDocs.docs.map((doc) => doc.id) await mapAsync(ids, async (id) => { diff --git a/test/fields/collections/Array/components/AddCustomBlocks/index.tsx b/test/fields/collections/Array/components/AddCustomBlocks/index.tsx index f2e0a2231..dad127771 100644 --- a/test/fields/collections/Array/components/AddCustomBlocks/index.tsx +++ b/test/fields/collections/Array/components/AddCustomBlocks/index.tsx @@ -8,7 +8,7 @@ const baseClass = 'custom-blocks-field-management' export const AddCustomBlocks: React.FC = () => { const { addFieldRow, replaceFieldRow } = useForm() - const { value } = useField({ path: 'customBlocks' }) + const { value } = useField({ path: 'customBlocks' }) return (
@@ -47,12 +47,12 @@ export const AddCustomBlocks: React.FC = () => { replaceFieldRow({ data: { block1Title: 'REPLACED BLOCK', blockType: 'block-1' }, path: 'customBlocks', - rowIndex: (Array.isArray(value) ? value.length : 0) - 1, + rowIndex: value - 1, }) } type="button" > - Replace Block {Array.isArray(value) ? value.length : 0} + Replace Block {value}
diff --git a/test/fields/collections/Array/index.ts b/test/fields/collections/Array/index.ts index 89c4b08b2..d55b32c6e 100644 --- a/test/fields/collections/Array/index.ts +++ b/test/fields/collections/Array/index.ts @@ -83,6 +83,16 @@ const ArrayFields: CollectionConfig = { type: 'text', name: 'text', }, + { + type: 'group', + name: 'groupInRow', + fields: [ + { + type: 'text', + name: 'textInGroupInRow', + }, + ], + }, ], }, { diff --git a/test/fields/collections/Lexical/blocks.ts b/test/fields/collections/Lexical/blocks.ts new file mode 100644 index 000000000..6b04794cd --- /dev/null +++ b/test/fields/collections/Lexical/blocks.ts @@ -0,0 +1,119 @@ +import type { Block } from '../../../../packages/payload/src/fields/config/types' + +import { lexicalEditor } from '../../../../packages/richtext-lexical/src' + +export const TextBlock: Block = { + fields: [ + { + name: 'text', + type: 'text', + required: true, + }, + ], + slug: 'text', +} + +export const RichTextBlock: Block = { + fields: [ + { + name: 'richText', + type: 'richText', + editor: lexicalEditor(), + }, + ], + slug: 'richText', +} + +export const UploadAndRichTextBlock: Block = { + fields: [ + { + name: 'upload', + type: 'upload', + relationTo: 'uploads', + required: true, + }, + { + name: 'richText', + type: 'richText', + editor: lexicalEditor(), + }, + ], + slug: 'uploadAndRichText', +} + +export const RelationshipBlock: Block = { + fields: [ + { + name: 'rel', + type: 'relationship', + relationTo: 'uploads', + required: true, + }, + ], + slug: 'relationshipBlock', +} + +export const SelectFieldBlock: Block = { + fields: [ + { + name: 'select', + type: 'select', + options: [ + { + label: 'Option 1', + value: 'option1', + }, + { + label: 'Option 2', + value: 'option2', + }, + { + label: 'Option 3', + value: 'option3', + }, + { + label: 'Option 4', + value: 'option4', + }, + { + label: 'Option 5', + value: 'option5', + }, + ], + }, + ], + slug: 'select', +} + +export const SubBlockBlock: Block = { + slug: 'subBlock', + fields: [ + { + name: 'subBlocks', + type: 'blocks', + blocks: [ + { + slug: 'contentBlock', + fields: [ + { + name: 'richText', + type: 'richText', + required: true, + editor: lexicalEditor(), + }, + ], + }, + { + slug: 'textArea', + fields: [ + { + name: 'content', + type: 'textarea', + required: true, + }, + ], + }, + ], + }, + ], +} diff --git a/test/fields/collections/Lexical/generateLexicalRichText.ts b/test/fields/collections/Lexical/generateLexicalRichText.ts new file mode 100644 index 000000000..4274abd48 --- /dev/null +++ b/test/fields/collections/Lexical/generateLexicalRichText.ts @@ -0,0 +1,193 @@ +export function generateLexicalRichText() { + return { + root: { + type: 'root', + format: '', + indent: 0, + version: 1, + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'Upload Node:', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + { + format: '', + type: 'upload', + version: 1, + fields: { + caption: { + root: { + type: 'root', + format: '', + indent: 0, + version: 1, + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'Relationship inside Upload Caption:', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + { + format: '', + type: 'relationship', + version: 1, + relationTo: 'text-fields', + value: { + id: '{{TEXT_DOC_ID}}', + }, + }, + ], + direction: 'ltr', + }, + }, + }, + relationTo: 'uploads', + value: { + id: '{{UPLOAD_DOC_ID}}', + }, + }, + { + format: '', + type: 'block', + version: 1, + fields: { + data: { + id: '65298b13db4ef8c744a7faaa', + rel: '{{UPLOAD_DOC_ID}}', + blockName: 'Block Node, with Relationship Field', + blockType: 'relationshipBlock', + }, + }, + }, + { + format: '', + type: 'block', + version: 1, + fields: { + data: { + id: '65298b1ddb4ef8c744a7faab', + richText: { + root: { + type: 'root', + format: '', + indent: 0, + version: 1, + children: [ + { + format: '', + type: 'relationship', + version: 1, + relationTo: 'text-fields', + value: { + id: '{{TEXT_DOC_ID}}', + }, + }, + ], + direction: null, + }, + }, + blockName: 'Block Node, with RichText Field, with Relationship Node', + blockType: 'richText', + }, + }, + }, + { + format: '', + type: 'block', + version: 1, + fields: { + data: { + id: '65298b2bdb4ef8c744a7faac', + blockName: + 'Block Node, with Blocks Field, With RichText Field, With Relationship Node', + blockType: 'subBlock', + subBlocks: [ + { + id: '65298b2edb4ef8c744a7faad', + richText: { + root: { + type: 'root', + format: '', + indent: 0, + version: 1, + children: [ + { + format: '', + type: 'relationship', + version: 1, + relationTo: 'text-fields', + value: { + id: '{{TEXT_DOC_ID}}', + }, + }, + ], + direction: null, + }, + }, + blockType: 'contentBlock', + }, + ], + }, + }, + }, + { + format: '', + type: 'block', + version: 1, + fields: { + data: { + id: '65298b49db4ef8c744a7faae', + upload: '{{UPLOAD_DOC_ID}}', + blockName: 'Block Node, With Upload Field', + blockType: 'uploadAndRichText', + }, + }, + }, + { + children: [], + direction: null, + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + { + children: [], + direction: null, + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + ], + direction: 'ltr', + }, + } +} diff --git a/test/fields/collections/Lexical/index.ts b/test/fields/collections/Lexical/index.ts new file mode 100644 index 000000000..914442b0f --- /dev/null +++ b/test/fields/collections/Lexical/index.ts @@ -0,0 +1,90 @@ +import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' + +import { + BlocksFeature, + LinkFeature, + TreeviewFeature, + UploadFeature, + lexicalEditor, +} from '../../../../packages/richtext-lexical/src' +import { + RelationshipBlock, + RichTextBlock, + SelectFieldBlock, + SubBlockBlock, + TextBlock, + UploadAndRichTextBlock, +} from './blocks' +import { generateLexicalRichText } from './generateLexicalRichText' + +export const LexicalFields: CollectionConfig = { + slug: 'lexical-fields', + admin: { + useAsTitle: 'title', + listSearchableFields: ['title', 'richTextLexicalCustomFields'], + }, + access: { + read: () => true, + }, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + { + name: 'richTextLexicalCustomFields', + type: 'richText', + required: true, + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + TreeviewFeature(), + LinkFeature({ + fields: [ + { + name: 'rel', + label: 'Rel Attribute', + type: 'select', + hasMany: true, + options: ['noopener', 'noreferrer', 'nofollow'], + admin: { + description: + 'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.', + }, + }, + ], + }), + UploadFeature({ + collections: { + uploads: { + fields: [ + { + name: 'caption', + type: 'richText', + editor: lexicalEditor(), + }, + ], + }, + }, + }), + BlocksFeature({ + blocks: [ + RichTextBlock, + TextBlock, + UploadAndRichTextBlock, + SelectFieldBlock, + RelationshipBlock, + SubBlockBlock, + ], + }), + ], + }), + }, + ], +} + +export const LexicalRichTextDoc = { + title: 'Rich Text', + richTextLexicalCustomFields: generateLexicalRichText(), +} diff --git a/test/fields/collections/Lexical/loremIpsum.ts b/test/fields/collections/Lexical/loremIpsum.ts new file mode 100644 index 000000000..d43b0805d --- /dev/null +++ b/test/fields/collections/Lexical/loremIpsum.ts @@ -0,0 +1,2 @@ +export const loremIpsum = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam hendrerit nisi sed sollicitudin pellentesque. Nunc posuere purus rhoncus pulvinar aliquam. Ut aliquet tristique nisl vitae volutpat. Nulla aliquet porttitor venenatis. Donec a dui et dui fringilla consectetur id nec massa. Aliquam erat volutpat. Sed ut dui ut lacus dictum fermentum vel tincidunt neque. Sed sed lacinia lectus. Duis sit amet sodales felis. Duis nunc eros, mattis at dui ac, convallis semper risus. In adipiscing ultrices tellus, in suscipit massa vehicula eu.' diff --git a/test/fields/config.ts b/test/fields/config.ts index 1ed2e9858..fa64a5d92 100644 --- a/test/fields/config.ts +++ b/test/fields/config.ts @@ -14,6 +14,7 @@ import DateFields, { dateDoc } from './collections/Date' import GroupFields, { groupDoc } from './collections/Group' import IndexedFields from './collections/Indexed' import JSONFields, { jsonDoc } from './collections/JSON' +import { LexicalFields, LexicalRichTextDoc } from './collections/Lexical' import NumberFields, { numberDoc } from './collections/Number' import PointFields, { pointDoc } from './collections/Point' import RadioFields, { radiosDoc } from './collections/Radio' @@ -41,6 +42,7 @@ export default buildConfigWithDefaults({ }), }, collections: [ + LexicalFields, { slug: 'users', auth: true, @@ -147,6 +149,14 @@ export default buildConfigWithDefaults({ .replace(/"\{\{TEXT_DOC_ID\}\}"/g, formattedTextID), ) + const lexicalRichTextDocWithRelId = JSON.parse( + JSON.stringify(LexicalRichTextDoc) + .replace(/"\{\{ARRAY_DOC_ID\}\}"/g, formattedID) + .replace(/"\{\{UPLOAD_DOC_ID\}\}"/g, formattedJPGID) + .replace(/"\{\{TEXT_DOC_ID\}\}"/g, formattedTextID), + ) + await payload.create({ collection: 'lexical-fields', data: lexicalRichTextDocWithRelId }) + const richTextDocWithRelationship = { ...richTextDocWithRelId } await payload.create({ collection: 'rich-text-fields', data: richTextBulletsDocWithRelId }) diff --git a/test/fields/e2e.spec.ts b/test/fields/e2e.spec.ts index f1073693a..81c75aea8 100644 --- a/test/fields/e2e.spec.ts +++ b/test/fields/e2e.spec.ts @@ -505,120 +505,117 @@ describe('fields', () => { }) describe('row manipulation', () => { - test('should add 2 new rows', async () => { + test('should add, remove and duplicate rows', async () => { + const assertText0 = 'array row 1' + const assertGroupText0 = 'text in group in row 1' + const assertText1 = 'array row 2' + const assertText3 = 'array row 3' + const assertGroupText3 = 'text in group in row 3' await page.goto(url.create) + // Add 3 rows await page.locator('#field-potentiallyEmptyArray > .array-field__add-row').click() await page.locator('#field-potentiallyEmptyArray > .array-field__add-row').click() - await page.locator('#field-potentiallyEmptyArray__0__text').fill('array row 1') - await page.locator('#field-potentiallyEmptyArray__1__text').fill('array row 2') - - await saveDocAndAssert(page) - }) - - test('should remove 2 new rows', async () => { - await page.goto(url.create) - await page.locator('#field-potentiallyEmptyArray > .array-field__add-row').click() - await page.locator('#field-potentiallyEmptyArray > .array-field__add-row').click() - await page.locator('#field-potentiallyEmptyArray__0__text').fill('array row 1') - await page.locator('#field-potentiallyEmptyArray__1__text').fill('array row 2') - await page.locator('#potentiallyEmptyArray-row-1 .array-actions__button').click() + // Fill out row 1 + await page.locator('#field-potentiallyEmptyArray__0__text').fill(assertText0) await page - .locator('#potentiallyEmptyArray-row-1 .popup__scroll-container .array-actions__remove') + .locator('#field-potentiallyEmptyArray__0__groupInRow__textInGroupInRow') + .fill(assertGroupText0) + // Fill out row 2 + await page.locator('#field-potentiallyEmptyArray__1__text').fill(assertText1) + // Fill out row 3 + await page.locator('#field-potentiallyEmptyArray__2__text').fill(assertText3) + await page + .locator('#field-potentiallyEmptyArray__2__groupInRow__textInGroupInRow') + .fill(assertGroupText3) + + // Remove row 1 + await page.locator('#potentiallyEmptyArray-row-0 .array-actions__button').click() + await page + .locator('#potentiallyEmptyArray-row-0 .popup__scroll-container .array-actions__remove') .click() + // Remove row 2 await page.locator('#potentiallyEmptyArray-row-0 .array-actions__button').click() await page .locator('#potentiallyEmptyArray-row-0 .popup__scroll-container .array-actions__remove') .click() - const rows = page.locator('#field-potentiallyEmptyArray > .array-field__draggable-rows') - - await expect(rows).toBeHidden() - }) - - test('should remove existing row', async () => { - await page.goto(url.create) - - await page.locator('#field-potentiallyEmptyArray > .array-field__add-row').click() - await page.locator('#field-potentiallyEmptyArray__0__text').fill('array row 1') - + // Save document await saveDocAndAssert(page) + // Scroll to array row (fields are not rendered in DOM until on screen) + await page.locator('#field-potentiallyEmptyArray__0__groupInRow').scrollIntoViewIfNeeded() + + // Expect the remaining row to be the third row + const input = page.locator('#field-potentiallyEmptyArray__0__groupInRow__textInGroupInRow') + await expect(input).toHaveValue(assertGroupText3) + + // Duplicate row await page.locator('#potentiallyEmptyArray-row-0 .array-actions__button').click() await page .locator( - '#potentiallyEmptyArray-row-0 .popup__scroll-container .array-actions__action.array-actions__remove', + '#potentiallyEmptyArray-row-0 .popup__scroll-container .array-actions__duplicate', ) .click() - const rows = page.locator('#field-potentiallyEmptyArray > .array-field__draggable-rows') + // Update duplicated row group field text + await page + .locator('#field-potentiallyEmptyArray__1__groupInRow__textInGroupInRow') + .fill(`${assertGroupText3} duplicate`) - await expect(rows).toBeHidden() + // Save document + await saveDocAndAssert(page) + + // Expect the second row to be a duplicate of the remaining row + await expect( + page.locator('#field-potentiallyEmptyArray__1__groupInRow__textInGroupInRow'), + ).toHaveValue(`${assertGroupText3} duplicate`) + + // Remove row 1 + await page.locator('#potentiallyEmptyArray-row-0 .array-actions__button').click() + await page + .locator('#potentiallyEmptyArray-row-0 .popup__scroll-container .array-actions__remove') + .click() + + // Save document + await saveDocAndAssert(page) + + // Expect the remaining row to be the copy of the duplicate row + await expect( + page.locator('#field-potentiallyEmptyArray__0__groupInRow__textInGroupInRow'), + ).toHaveValue(`${assertGroupText3} duplicate`) }) - test('should add row after removing existing row', async () => { - await page.goto(url.create) + describe('react hooks', () => { + test('should add 2 new block rows', async () => { + await page.goto(url.create) - await page.locator('#field-potentiallyEmptyArray > .array-field__add-row').click() - await page.locator('#field-potentiallyEmptyArray > .array-field__add-row').click() - await page.locator('#field-potentiallyEmptyArray__0__text').fill('array row 1') - await page.locator('#field-potentiallyEmptyArray__1__text').fill('array row 2') + await page + .locator('.custom-blocks-field-management') + .getByRole('button', { name: 'Add Block 1' }) + .click() + await expect( + page.locator('#field-customBlocks input[name="customBlocks.0.block1Title"]'), + ).toHaveValue('Block 1: Prefilled Title') - await saveDocAndAssert(page) + await page + .locator('.custom-blocks-field-management') + .getByRole('button', { name: 'Add Block 2' }) + .click() + await expect( + page.locator('#field-customBlocks input[name="customBlocks.1.block2Title"]'), + ).toHaveValue('Block 2: Prefilled Title') - await page.locator('#potentiallyEmptyArray-row-1 .array-actions__button').click() - await page - .locator( - '#potentiallyEmptyArray-row-1 .popup__scroll-container .array-actions__action.array-actions__remove', - ) - .click() - await page.locator('#field-potentiallyEmptyArray > .array-field__add-row').click() - - await page.locator('#field-potentiallyEmptyArray__1__text').fill('updated array row 2') - - await saveDocAndAssert(page) - - const rowsContainer = page.locator( - '#field-potentiallyEmptyArray > .array-field__draggable-rows', - ) - const directChildDivCount = await rowsContainer.evaluate((element) => { - const childDivCount = element.querySelectorAll(':scope > div') - return childDivCount.length + await page + .locator('.custom-blocks-field-management') + .getByRole('button', { name: 'Replace Block 2' }) + .click() + await expect( + page.locator('#field-customBlocks input[name="customBlocks.1.block1Title"]'), + ).toHaveValue('REPLACED BLOCK') }) - - expect(directChildDivCount).toBe(2) - }) - }) - - describe('row react hooks', () => { - test('should add 2 new block rows', async () => { - await page.goto(url.create) - - await page - .locator('.custom-blocks-field-management') - .getByRole('button', { name: 'Add Block 1' }) - .click() - await expect( - page.locator('#field-customBlocks input[name="customBlocks.0.block1Title"]'), - ).toHaveValue('Block 1: Prefilled Title') - - await page - .locator('.custom-blocks-field-management') - .getByRole('button', { name: 'Add Block 2' }) - .click() - await expect( - page.locator('#field-customBlocks input[name="customBlocks.1.block2Title"]'), - ).toHaveValue('Block 2: Prefilled Title') - - await page - .locator('.custom-blocks-field-management') - .getByRole('button', { name: 'Replace Block 2' }) - .click() - await expect( - page.locator('#field-customBlocks input[name="customBlocks.1.block1Title"]'), - ).toHaveValue('REPLACED BLOCK') }) }) }) diff --git a/test/nested-fields/e2e.spec.ts b/test/nested-fields/e2e.spec.ts deleted file mode 100644 index 1b121df84..000000000 --- a/test/nested-fields/e2e.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Page } from '@playwright/test' - -import { expect, test } from '@playwright/test' - -import { saveDocAndAssert } from '../helpers' -import { AdminUrlUtil } from '../helpers/adminUrlUtil' -import { initPayloadTest } from '../helpers/configHelpers' - -const { beforeAll, describe } = test -let url: AdminUrlUtil - -const slug = 'nested-fields' - -let page: Page - -describe('Nested Fields', () => { - beforeAll(async ({ browser }) => { - const { serverURL } = await initPayloadTest({ - __dirname, - init: { - local: false, - }, - }) - - url = new AdminUrlUtil(serverURL, slug) - - const context = await browser.newContext() - page = await context.newPage() - }) - - test('should save deeply nested fields', async () => { - const assertionValue = 'sample block value' - - await page.goto(url.create) - - await page.locator('#field-array > button').click() - await page.locator('#field-array__0__group__namedTab__blocks > button').click() - await page.locator('button[title="Block With Field"]').click() - - await page.locator('#field-array__0__group__namedTab__blocks__0__text').fill(assertionValue) - - await saveDocAndAssert(page) - - await expect(page.locator('#field-array__0__group__namedTab__blocks__0__text')).toHaveValue( - assertionValue, - ) - }) -}) diff --git a/test/versions/int.spec.ts b/test/versions/int.spec.ts index 253a94052..c02efe0bf 100644 --- a/test/versions/int.spec.ts +++ b/test/versions/int.spec.ts @@ -6,7 +6,7 @@ import { initPayloadTest } from '../helpers/configHelpers' import AutosavePosts from './collections/Autosave' import configPromise from './config' import AutosaveGlobal from './globals/Autosave' -import { autosaveSlug } from './shared' +import { autosaveSlug, draftSlug } from './shared' let collectionLocalPostID: string let collectionLocalVersionID @@ -200,6 +200,44 @@ describe('Versions', () => { expect(versions.docs[0].version.title.en).toStrictEqual(newEnglishTitle) expect(versions.docs[0].version.title.es).toStrictEqual(spanishTitle) }) + + it('should query drafts with sort', async () => { + const draftsAscending = await payload.find({ + collection: draftSlug, + draft: true, + sort: 'title', + }) + const draftsDescending = await payload.find({ + collection: draftSlug, + draft: true, + sort: '-title', + }) + + expect(draftsAscending).toBeDefined() + expect(draftsDescending).toBeDefined() + expect(draftsAscending.docs[0]).toMatchObject( + draftsDescending.docs[draftsDescending.docs.length - 1], + ) + }) + + it('should findVersions with sort', async () => { + const draftsAscending = await payload.findVersions({ + collection: draftSlug, + draft: true, + sort: 'createdAt', + }) + const draftsDescending = await payload.findVersions({ + collection: draftSlug, + draft: true, + sort: '-updatedAt', + }) + + expect(draftsAscending).toBeDefined() + expect(draftsDescending).toBeDefined() + expect(draftsAscending.docs[0]).toMatchObject( + draftsDescending.docs[draftsDescending.docs.length - 1], + ) + }) }) describe('Restore', () => {