Compare commits

...

33 Commits

Author SHA1 Message Date
Jacob Fletcher
4dae26941f fix: bloated client config with disabled and hidden fields causing slow lexical 2024-12-06 09:25:55 -05:00
Nikola Spalevic
10eab87653 chore(translations): improved serbian translations for the lexical editor (#9795)
### What?

Enhanced Serbian translations for the lexical editor have been
implemented. The updates correct inaccuracies in the Serbian Cyrillic
translations and address various errors in the previous versions.


### Why?

- Incorrect use of Latin script in place of Cyrillic.
- Contextual errors in translations.
2024-12-06 09:10:17 -05:00
Elliot DeNolf
bbf35a62d8 ci: explicitly use ubuntu-24.04 instead of latest to ensure compat (#9786)
The runner image `ubuntu-latest` image will be switching from Ubuntu
22.04 to Ubuntu 24.04 as specified in
https://github.com/actions/runner-images/issues/10636.

> Rollout will begin on December 5th and will complete on January 17th,
2025.
Breaking changes 
Ubuntu 24.04 is ready to be the default version for the "ubuntu-latest"
label in GitHub Actions and Azure DevOps.

This PR moves us to explicitly use `ubuntu-24.04` to ensure
compatibility and to allow explicit upgrades in the future.
2024-12-06 02:27:10 -05:00
Elliot DeNolf
a108986f1b ci: fetch-depth 0 needed for lint job 2024-12-06 02:12:06 -05:00
Elliot DeNolf
4cc6f4cee4 ci: main workflow improvements (#9784)
Speed up and refactor main workflow:

- Take advantage of re-usable node/pnpm setup action
- Remove explicit fetch-depth
- Clean up timeout-minutes
2024-12-06 02:03:45 -05:00
Paul
cb691e0642 ci: only run tests when needed via needs_tests filter (#9781)
Only run tests when needed via `needs_tests` filter. Useful to avoid
running test suite with template changes.
2024-12-05 23:19:34 -05:00
Said Akhrarov
c9ce3501a0 docs(plugin-search): add info on collection reindexing (#9764)
Adds documentation for the feature introduced with [plugin-search
collection reindexing](https://github.com/payloadcms/payload/pull/9391).
This also fixes an invalid scss import in one of the examples.

Credit to @rilrom for the invalid css import find!
2024-12-05 17:13:51 -05:00
Elliot DeNolf
ef8d3c9bf4 ci: post-release-templates assign PR to user that triggered 2024-12-05 16:34:54 -05:00
Elliot DeNolf
d3232b947d templates: bump for v3.4.0 (#9780)
Automated bump of templates for v3.4.0

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-05 15:33:33 -06:00
Elliot DeNolf
28c6b2a66b ci: post-release-templates always use latest tag with workflow_dispatch 2024-12-05 16:26:26 -05:00
Dan Ribbens
a11243e288 fix(ui): join field ignoring defaultSort and defaultLimit (#9766)
The join field was not respecting the defaultSort or defaultLimit of the
field configuration.

### Why?

This was never implemented.

### How?

This fix applies these correct limit and sort properties to the query,
first based on the field config and as a fallback, the collection
configuration.
2024-12-05 21:23:19 +00:00
Paul
19ddd3cfc6 templates: improvements to seed speed on website template and updated hero and collapsible fields (#9779)
- Improvements to seed speed on the website template
- Update hero on mobile
- Fields are collapsed by default where possible now
- Add rowlabel components for nav items
2024-12-05 15:08:02 -06:00
Said Akhrarov
1ab3be65f8 fix(ui): disable doc submenu when parent button is disabled (#9750) 2024-12-05 15:56:00 -05:00
Sasha
de53f2a826 docs: adds missing "to" in jobs-queue/overview (#9778)
Adds missing "to":
"offload these tasks a separate compute resource"
 ->
 "offload these tasks **_to_** a separate compute resource".
2024-12-05 19:38:16 +00:00
Sasha
7def6b7ddf fix: defaultPopulate and populate with nested to arrays/blocks properties (#9751)
Fixes https://github.com/payloadcms/payload/issues/9718

### What?
`defaultPopulate` and `populate` didn't work properly when defining
nested to arrays and blocks properties:
```ts
import type { CollectionConfig } from 'payload'
export const Pages: CollectionConfig<'pages'> = {
  slug: 'pages',
  defaultPopulate: {
    slug: true,
    array: {
      title: true,
    },
    blocks: {
      some: {
        title: true,
      },
    },
  },
  access: { read: () => true },
  fields: [
    {
      name: 'slug',
      type: 'text',
      required: true,
    },
    {
      name: 'additional',
      type: 'text',
    },
    {
      name: 'array',
      type: 'array',
      fields: [
        {
          name: 'title',
          type: 'text',
        },
        {
          name: 'other',
          type: 'text',
        },
      ],
    },
    {
      name: 'blocks',
      type: 'blocks',
      blocks: [
        {
          slug: 'some',
          fields: [
            {
              name: 'title',
              type: 'text',
            },
            {
              name: 'other',
              type: 'text',
            },
          ],
        },
      ],
    },
  ],
}

``` 

### Why?
This should work

### How?
Turns out, it wasn't a great idea to mutate passed `select` directly in
`afterRead/promise.ts` to force select some properties `id` ,
`blockType`. Now we do shallow copies when needed.
2024-12-05 12:29:22 -05:00
Sasha
840dde2b17 fix(db-mongodb): bump mongoose to 8.8.3 (#9747)
Fixes https://github.com/payloadcms/payload/issues/9729. The current
version has vulnerability
https://avd.aquasec.com/nvd/2024/cve-2024-53900/. Technically, Payload
doesn't use described in the report
[`$where`](https://www.mongodb.com/docs/manual/reference/operator/query/where/#op._S_where)
property in its queries at all, but it may affect those who access
mongoose via `payload.db.collections` directly
2024-12-05 18:43:14 +02:00
Elliot DeNolf
c2ff9b1dd8 ci: use PAT for post-release-templates 2024-12-05 10:51:12 -05:00
github-actions[bot]
97aca5fde7 templates: bump for v3.4.0 (#9765)
Automated bump of templates for v3.4.0

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-05 05:12:46 +00:00
Paul
97d3bb1c11 templates(website): add next sitemap robots disallow config for /admin (#9761)
Sets robots disallow to /admin
2024-12-05 01:30:40 +00:00
Paul
8f785e1fde chore(ui): expose onInputChange from react-select in SelectInput component (#9728) 2024-12-04 19:27:14 -06:00
Germán Jabloñski
89db8fb7f2 chore(templates): migrate to new richtext component in website template (#9615)
In addition to requiring fewer files, it supports more nodes. If you
currently initialize a website template and want to use features such as
images or tables, they are not rendered. With this change that happens
automatically.

Credits to @AlessioGr for the [JSX
serializer](https://github.com/payloadcms/payload/pull/8795).

---------

Co-authored-by: Paul Popus <paul@nouance.io>
2024-12-05 00:38:49 +00:00
Paul
3d1305de5c templates: fixes the seeding for the website template when using postgres (#9758) 2024-12-04 23:58:57 +00:00
Sasha
0829a350ce feat: allow to define global label as function (#9759) 2024-12-04 18:39:35 -05:00
Jacob Fletcher
1fc9c47f20 feat(next): supports relative preview URLs (#9755)
Similar to #9746. When deploying to Vercel, preview deployment URLs are
dynamically generated. This breaks `admin.preview` within those
deployments because there is no mechanism by which we can detect and set
that URL within Payload. Although Vercel provides various environment
variables at our disposal, they provide no concrete identifier for
exactly which URL is being currently previewed (you can access the same
deployment from a number of different URLs).

The fix is to support relative `admin.preview` URLs, that way Payload
can prepend the application's top-level domain dynamically at
render-time in order to create a fully qualified URL. So when you visit
a Vercel preview deployment, for example, that deployment's unique URL
is used as the preview redirect, instead of the application's
root/production domain. Note: this does not fix multi-tenancy
single-domain setups, as those still require a static top-level domain
for each tenant.
2024-12-04 17:01:09 -05:00
Alessio Gravili
61a4656ef5 chore(richtext-lexical): remove outdated custom block component examples (#9754) 2024-12-04 19:58:50 +00:00
Sasha
d8f7034ab8 fix: getPayload generate import map only when used in Payload Admin Panel (#9371)
After the change with removing `getPayloadHMR`, we do generate import
map even outside of Next.js, which leads to errors when using in a
project without it:

![image](https://github.com/user-attachments/assets/e8dc2af6-566b-443c-a6d8-8b02e719bd30)
2024-12-04 19:46:45 +00:00
github-actions[bot]
fa39b37a44 templates: bump for v3.4.0 (#9752)
Automated bump of templates for v3.4.0

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-04 19:46:32 +00:00
Jarrod Flesch
fa7ed3f621 fix(ui): stale locale value from useLocale (#9582)
### What?
Fixes issue with stale locale from searchParams

### Why?
Bad use of useEffect/useState inside our useSearchParams provider.

### How?
Memoize the locale instead of relying on the useEffect which was causing
unnecessary renders with stale values.
2024-12-04 14:00:17 -05:00
Paul
2321970fcc templates: improve speed of seed script (#9748)
Improves the speed of the seed script by moving operations to be async
and disabling revalidation on them
2024-12-04 12:38:18 -06:00
Alessio Gravili
84a5b4066d ci: ensure clean all script does not error after retrying step, by installing globby and chalk globally (#9745) 2024-12-04 18:37:46 +00:00
Jacob Fletcher
f12b4dc6b0 feat(live-preview): supports relative urls for dynamic preview deployments (#9746)
When deploying to Vercel, preview deployment URLs are dynamically
generated. This breaks Live Preview within those deployments because
there is no mechanism by which we can detect and set that URL within
Payload. Although Vercel provides various environment variables at our
disposal, they provide no concrete identifier for exactly _which_ URL is
being currently previewed (you an access the same deployment from a
number of different URLs).

The fix is to support _relative_ live preview URLs, that way Payload can
prepend the application's top-level domain dynamically at render-time in
order to create a fully qualified URL. So when you visit a Vercel
preview deployment, for example, that deployment's unique URL is used to
load the iframe of the preview window, instead of the application's
root/production domain. Note: this does not fix multi-tenancy
single-domain setups, as those still require a static top-level domain
for each tenant.
2024-12-04 13:31:43 -05:00
Jarrod Flesch
8e26824bf8 fix(ui): only render header dom node if needed (#9742)
### What?
The `<header>` dom node was rendering even if empty for group fields.
Causing extra margin to be added even if no label/description were
provided.

### Why?
If the field had no label, description or errors it would still render.

### How?
Wraps the header node in an additional condition that checks for label,
description or errors before rendering the node.
2024-12-04 13:29:45 -05:00
Elliot DeNolf
12a8bba852 ci: ensure triage actions work for PRs from forks 2024-12-04 11:35:38 -05:00
162 changed files with 1759 additions and 1672 deletions

View File

@@ -1,15 +1,33 @@
name: Setup node and pnpm
description: Configure the Node.js and pnpm versions
description: |
Configures Node, pnpm, cache, performs pnpm install
inputs:
node-version:
description: 'The Node.js version to use'
description: Node.js version
required: true
default: 22.6.2
default: 22.6.0
pnpm-version:
description: 'The pnpm version to use'
description: Pnpm version
required: true
default: 9.7.1
pnpm-run-install:
description: Whether to run pnpm install
required: false
default: true
pnpm-restore-cache:
description: Whether to restore cache
required: false
default: true
pnpm-install-cache-key:
description: The cache key for the pnpm install cache
default: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
outputs:
pnpm-store-path:
description: The resolved pnpm store path
pnpm-install-cache-key:
description: The cache key used for pnpm install cache
runs:
using: composite
@@ -30,19 +48,29 @@ runs:
version: ${{ inputs.pnpm-version }}
run_install: false
- name: Get pnpm store directory
- name: Get pnpm store path
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
STORE_PATH=$(pnpm store path --silent)
echo "STORE_PATH=$STORE_PATH" >> $GITHUB_ENV
echo "Pnpm store path resolved to: $STORE_PATH"
- name: Setup pnpm cache
- name: Restore pnpm install cache
if: ${{ inputs.pnpm-restore-cache == 'true' }}
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
key: ${{ inputs.pnpm-install-cache-key }}
restore-keys: |
pnpm-store-${{ inputs.pnpm-version }}-
pnpm-store-
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- shell: bash
- name: Run pnpm install
if: ${{ inputs.pnpm-run-install == 'true' }}
shell: bash
run: pnpm install
# Set the cache key output
- run: |
echo "pnpm-install-cache-key=${{ inputs.pnpm-install-cache-key }}" >> $GITHUB_ENV
shell: bash

View File

@@ -13,7 +13,7 @@ on:
jobs:
on-labeled-ensure-one-status:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
issues: write
# Only run on issue labeled and if label starts with 'status:'
@@ -49,7 +49,7 @@ jobs:
}
on-issue-close:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
issues: write
if: github.event.action == 'closed'
@@ -82,7 +82,7 @@ jobs:
}
on-issue-reopen:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
issues: write
if: github.event.action == 'reopened'
@@ -93,7 +93,7 @@ jobs:
labels: 'status: needs-triage'
on-issue-assigned:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
issues: write
if: >
@@ -106,11 +106,11 @@ jobs:
labels: 'status: needs-triage'
# on-pr-merge:
# runs-on: ubuntu-latest
# runs-on: ubuntu-24.04
# if: github.event.pull_request.merged == true
# steps:
# on-pr-close:
# runs-on: ubuntu-latest
# runs-on: ubuntu-24.04
# if: github.event_name == 'pull_request_target' && github.event.pull_request.merged == false
# steps:

View File

@@ -11,7 +11,7 @@ permissions:
jobs:
lock_issues:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: Lock issues
uses: dessant/lock-threads@v5

View File

@@ -23,11 +23,12 @@ env:
jobs:
changes:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
pull-requests: read
outputs:
needs_build: ${{ steps.filter.outputs.needs_build }}
needs_tests: ${{ steps.filter.outputs.needs_tests }}
templates: ${{ steps.filter.outputs.templates }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
@@ -35,8 +36,6 @@ jobs:
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
with:
fetch-depth: 25
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -48,53 +47,36 @@ jobs:
- 'pnpm-lock.yaml'
- 'package.json'
- 'templates/**'
needs_tests:
- '.github/workflows/**'
- 'packages/**'
- 'test/**'
- 'pnpm-lock.yaml'
- 'package.json'
templates:
- 'templates/**'
- name: Log all filter results
run: |
echo "needs_build: ${{ steps.filter.outputs.needs_build }}"
echo "needs_tests: ${{ steps.filter.outputs.needs_tests }}"
echo "templates: ${{ steps.filter.outputs.templates }}"
lint:
if: >
github.event_name == 'pull_request' && !contains(github.event.pull_request.title, 'no-lint') && !contains(github.event.pull_request.title, 'skip-lint')
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
timeout-minutes: 720
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install
- name: Lint staged
run: |
git diff --name-only --diff-filter=d origin/${GITHUB_BASE_REF}...${GITHUB_SHA}
@@ -103,78 +85,46 @@ jobs:
build:
needs: changes
if: ${{ needs.changes.outputs.needs_build == 'true' }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 25
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
timeout-minutes: 720
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install
- run: pnpm run build:all
env:
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
- name: Cache build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
tests-unit:
runs-on: ubuntu-latest
needs: build
runs-on: ubuntu-24.04
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -185,8 +135,9 @@ jobs:
NODE_OPTIONS: --max-old-space-size=8096
tests-int:
runs-on: ubuntu-latest
needs: build
runs-on: ubuntu-24.04
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
name: int-${{ matrix.database }}
strategy:
fail-fast: false
@@ -222,24 +173,21 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 25
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Restore build
uses: actions/cache@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- run: pnpm install
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start LocalStack
run: pnpm docker:start
@@ -284,11 +232,12 @@ jobs:
max_attempts: 5
timeout_minutes: 15
command: pnpm test:int
on_retry_command: pnpm clean:all && pnpm install
on_retry_command: pnpm clean:build && pnpm install --no-frozen-lockfile
tests-e2e:
runs-on: ubuntu-latest
needs: build
runs-on: ubuntu-24.04
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
name: e2e-${{ matrix.suite }}
strategy:
fail-fast: false
@@ -331,24 +280,19 @@ jobs:
env:
SUITE_NAME: ${{ matrix.suite }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -386,7 +330,7 @@ jobs:
max_attempts: 5
timeout_minutes: 20
command: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.suite }}.json pnpm test:e2e:prod:ci ${{ matrix.suite }}
on_retry_command: pnpm clean:all && pnpm install && pnpm build:all
on_retry_command: pnpm clean:build && pnpm install --no-frozen-lockfile && pnpm build:all
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: results_${{ matrix.suite }}.json
NEXT_TELEMETRY_DISABLED: 1
@@ -408,7 +352,7 @@ jobs:
# Build listed templates with packed local packages
build-templates:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs: build
strategy:
matrix:
@@ -439,24 +383,19 @@ jobs:
POSTGRES_DB: payloadtests
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -492,28 +431,23 @@ jobs:
pnpm runts scripts/build-template-with-local-pkgs.ts ${{ matrix.template }} $POSTGRES_URL
tests-type-generation:
runs-on: ubuntu-latest
needs: build
runs-on: ubuntu-24.04
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -527,7 +461,7 @@ jobs:
all-green:
name: All Green
if: always()
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs:
- lint
- build
@@ -541,7 +475,7 @@ jobs:
publish-canary:
name: Publish Canary
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: ${{ needs.all-green.result == 'success' && github.ref_name == 'main' }}
needs:
- all-green

View File

@@ -5,10 +5,6 @@ on:
types:
- published
workflow_dispatch:
inputs:
tag:
description: 'Release tag to process (optional)'
required: true
env:
NODE_VERSION: 22.6.0
@@ -18,7 +14,7 @@ env:
jobs:
update_templates:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: write
@@ -64,12 +60,15 @@ jobs:
- name: Determine Release Tag
id: determine_tag
run: |
if [ "${{ github.event.inputs.tag }}" != "" ]; then
echo "Using tag from input: ${{ github.event.inputs.tag }}"
echo "release_tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
else
if [ "${{ github.event.release.tag_name }}" != "" ]; then
echo "Using tag from release event: ${{ github.event.release.tag_name }}"
echo "release_tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
else
# pull latest tag from github, must match any version except v2. Should match v3, v4, v99, etc.
echo "Fetching latest tag from github..."
LATEST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude 'v2*')
echo "Latest tag: $LATEST_TAG"
echo "release_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
fi
- name: Commit and push changes
@@ -83,17 +82,21 @@ jobs:
git diff --name-only
export BRANCH_NAME=templates/bump-${{ steps.determine_tag.outputs.release_tag }}
export BRANCH_NAME=templates/bump-${{ steps.determine_tag.outputs.release_tag }}-$(date +%s)
echo "branch=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
- name: Create pull request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.GH_TOKEN_POST_RELEASE_TEMPLATES }}
labels: 'area: templates'
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
commit-message: 'templates: bump templates for ${{ steps.determine_tag.outputs.release_tag }}'
branch: ${{ steps.commit.outputs.branch }}
base: main
assignees: ${{ github.actor }}
title: 'templates: bump for ${{ steps.determine_tag.outputs.release_tag }}'
body: 'Automated bump of templates for ${{ steps.determine_tag.outputs.release_tag }}'
body: |
🤖 Automated bump of templates for ${{ steps.determine_tag.outputs.release_tag }}
Triggered by user: @${{ github.actor }}

View File

@@ -19,7 +19,7 @@ env:
jobs:
post_release:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v4
@@ -38,7 +38,7 @@ jobs:
🚀 This is included in version {release_link}
github-releases-to-discord:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- name: Checkout

View File

@@ -1,7 +1,7 @@
name: pr-title
on:
pull_request:
pull_request_target:
types:
- opened
- edited
@@ -12,7 +12,7 @@ permissions:
jobs:
main:
name: lint-pr-title
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: amannn/action-semantic-pull-request@v5
id: lint_pr_title
@@ -107,7 +107,7 @@ jobs:
label-pr-on-open:
name: label-pr-on-open
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: github.event.action == 'opened'
steps:
- name: Tag with 2.x branch with v2

View File

@@ -14,7 +14,7 @@ jobs:
name: release-canary-${{ github.ref_name }}-${{ github.sha }}
permissions:
id-token: write
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4

View File

@@ -5,7 +5,7 @@ on:
jobs:
stale:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
issues: write
pull-requests: write

View File

@@ -1,7 +1,7 @@
name: triage
on:
pull_request:
pull_request_target:
types:
- opened
issues:
@@ -18,7 +18,7 @@ permissions:
jobs:
debug-context:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: View context attributes
uses: actions/github-script@v7
@@ -27,11 +27,10 @@ jobs:
label-created-by:
name: label-on-open
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: Tag with 'created-by'
uses: actions/github-script@v7
if: github.event.action == 'opened'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -89,10 +88,11 @@ jobs:
triage:
name: initial-triage
if: github.event_name == 'issues'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}
token: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/actions/triage
with:

View File

@@ -108,6 +108,8 @@ export const Posts: CollectionConfig = {
}
```
The `preview` property resolves to a string that points to your front-end application with additional URL parameters. This can be an absolute URL or a relative path. If you are using a relative path, Payload will prepend the application's origin onto it, creating a fully qualified URL.
The preview function receives two arguments:
| Argument | Description |

View File

@@ -445,7 +445,7 @@ Then to colorize your Custom Component's background, for example, you can use th
Payload also exports its [SCSS](https://sass-lang.com) library for reuse which includes mixins, etc. To use this, simply import it as follows into your `.scss` file:
```scss
@import '~payload/scss';
@import '~@payloadcms/ui/scss';
.my-component {
@include mid-break {

View File

@@ -42,7 +42,7 @@ Examples:
**Offloading complex operations**
You may run into the need to perform computationally expensive functions which might slow down your main Payload API server(s). The Jobs Queue allows you to offload these tasks a separate compute resource rather than slowing down the server(s) that run your Payload APIs. With Payload Task definitions, you can even keep large dependencies out of your main Next.js bundle by dynamically importing them only when they are used. This keeps your Next.js + Payload compilation fast and ensures large dependencies do not get bundled into your Payload production build.
You may run into the need to perform computationally expensive functions which might slow down your main Payload API server(s). The Jobs Queue allows you to offload these tasks to a separate compute resource rather than slowing down the server(s) that run your Payload APIs. With Payload Task definitions, you can even keep large dependencies out of your main Next.js bundle by dynamically importing them only when they are used. This keeps your Next.js + Payload compilation fast and ensures large dependencies do not get bundled into your Payload production build.
Examples:

View File

@@ -52,7 +52,9 @@ _\* An asterisk denotes that a property is required._
### URL
The `url` property is a string that points to your front-end application. This value is used as the `src` attribute of the iframe rendering your front-end. Once loaded, the Admin Panel will communicate directly with your app through `window.postMessage` events.
The `url` property resolves to a string that points to your front-end application. This value is used as the `src` attribute of the iframe rendering your front-end. Once loaded, the Admin Panel will communicate directly with your app through `window.postMessage` events.
This can be an absolute URL or a relative path. If you are using a relative path, Payload will prepend the application's origin onto it, creating a fully qualified URL. This is useful for Vercel preview deployments, for example, where URLs are not known ahead of time.
To set the URL, use the `admin.livePreview.url` property in your [Payload Config](../configuration/overview):

View File

@@ -33,6 +33,7 @@ This plugin is a great way to implement a fast, immersive search experience such
- Allows you to query search results using first-party Payload APIs
- Allows you to query documents without triggering any of their underlying hooks
- Allows you to easily prioritize search results by collection or document
- Allows you to reindex search results by collection on demand
## Installation
@@ -81,7 +82,7 @@ export default config
The `collections` property is an array of collection slugs to enable syncing to search. Enabled collections receive a `beforeChange` and `afterDelete` hook that creates, updates, and deletes its respective search record as it changes over time.
### `localize`
#### `localize`
By default, the search plugin will add `localization: true` to the `title` field of the newly added `search` collection if you have localization enabled. If you would like to disable this behavior, you can set this to `false`.
@@ -159,6 +160,14 @@ When `syncDrafts` is true, draft documents will be synced to search. This is fal
If true, will delete documents from search whose status changes to draft. This is true by default. You must have [Payload Drafts](https://payloadcms.com/docs/versions/drafts) enabled for this to apply.
#### `reindexBatchSize`
A number that, when specified, will be used as the value to determine how many search documents to fetch for reindexing at a time in each batch. If not set, this will default to `50`.
### Collection reindexing
Collection reindexing allows you to recreate search documents from your search-enabled collections on demand. This is useful if you have existing documents that don't already have search indexes, commonly when adding `plugin-search` to an existing project. To get started, navigate to your search collection and click the pill in the top right actions slot of the list view labelled `Reindex`. This will open a popup with options to select one of your search-enabled collections, or all, for reindexing.
## TypeScript
All types can be directly imported:

View File

@@ -1,4 +1,4 @@
import { withPayload } from "@payloadcms/next/withPayload";
import { withPayload } from '@payloadcms/next/withPayload'
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {}

View File

@@ -1,14 +1,14 @@
{
"name": "payload-3-custom-server",
"type": "module",
"scripts": {
"dev": "nodemon",
"build": "next build && tsc --project tsconfig.server.json",
"start": "cross-env NODE_ENV=production node dist/server.js",
"dev": "nodemon",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload"
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"start": "cross-env NODE_ENV=production node dist/server.js"
},
"type": "module",
"dependencies": {
"@payloadcms/db-mongodb": "latest",
"@payloadcms/next": "latest",

View File

@@ -1,3 +1,3 @@
export default function B() {
return <div>b</div>;
return <div>b</div>
}

View File

@@ -1,11 +1,7 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
)
}

View File

@@ -1,5 +1 @@
export const importMap = {
}
export const importMap = {}

View File

@@ -47,7 +47,7 @@
},
"dependencies": {
"http-status": "1.6.2",
"mongoose": "8.8.1",
"mongoose": "8.8.3",
"mongoose-aggregate-paginate-v2": "1.1.2",
"mongoose-paginate-v2": "1.8.5",
"prompts": "2.4.2",

View File

@@ -34,6 +34,11 @@ export const preview: CollectionRouteHandlerWithID = async ({ id, collection, re
req,
token,
})
// Support relative URLs by prepending the origin, if necessary
if (previewURL && previewURL.startsWith('/')) {
previewURL = `${req.protocol}//${req.host}${previewURL}`
}
} catch (err) {
return routeError({
collection,

View File

@@ -36,7 +36,7 @@ export const LivePreviewView: PayloadServerReactComponent<EditViewComponent> = a
},
]
const url =
let url =
typeof livePreviewConfig?.url === 'function'
? await livePreviewConfig.url({
collectionConfig,
@@ -47,5 +47,10 @@ export const LivePreviewView: PayloadServerReactComponent<EditViewComponent> = a
})
: livePreviewConfig?.url
// Support relative URLs by prepending the origin, if necessary
if (url && url.startsWith('/')) {
url = `${initPageResult.req.protocol}//${initPageResult.req.host}${url}`
}
return <LivePreviewClient breakpoints={breakpoints} initialData={doc} url={url} />
}

View File

@@ -1,7 +1,7 @@
import type { ImportMap } from '../../bin/generateImportMap/index.js'
import type { SanitizedConfig } from '../../config/types.js'
import type { PaginatedDocs } from '../../database/types.js'
import type { PayloadRequest, Where } from '../../types/index.js'
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
export type DefaultServerFunctionArgs = {
importMap: ImportMap
@@ -43,7 +43,7 @@ export type ListQuery = {
When provided, is automatically injected into the `where` object
*/
search?: string
sort?: string
sort?: Sort
where?: Where
}

View File

@@ -106,12 +106,15 @@ export const createClientCollectionConfig = ({
if (serverOnlyCollectionProperties.includes(key as any)) {
continue
}
switch (key) {
case 'admin':
if (!collection.admin) {
break
}
clientCollection.admin = {} as ClientCollectionConfig['admin']
for (const adminKey in collection.admin) {
if (serverOnlyCollectionAdminProperties.includes(adminKey as any)) {
continue

View File

@@ -18,7 +18,7 @@ import type { Payload } from '../../types/index.js'
import { getFromImportMap } from '../../bin/generateImportMap/getFromImportMap.js'
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
import { fieldAffectsData } from '../../fields/config/types.js'
import { fieldAffectsData, fieldIsHiddenOrDisabled } from '../../fields/config/types.js'
import { flattenTopLevelFields, type ImportMap } from '../../index.js'
import { removeUndefined } from '../../utilities/removeUndefined.js'
@@ -75,6 +75,24 @@ export const createClientField = ({
}): ClientField => {
const clientField: ClientField = {} as ClientField
if (fieldAffectsData(incomingField) && fieldIsHiddenOrDisabled(incomingField)) {
clientField.type = incomingField.type
if (incomingField.hidden) {
clientField.hidden = true
}
if (incomingField.admin?.disabled) {
if (!clientField.admin) {
clientField.admin = {} as AdminClient
}
clientField.admin.disabled = true
}
return clientField
}
for (const key in incomingField) {
if (serverOnlyFieldProperties.includes(key as any)) {
continue

View File

@@ -1410,7 +1410,10 @@ export type JoinField = {
export type JoinFieldClient = {
admin?: AdminClient & Pick<JoinField['admin'], 'allowCreate' | 'disableBulkEdit' | 'readOnly'>
} & FieldBaseClient &
Pick<JoinField, 'collection' | 'index' | 'maxDepth' | 'on' | 'type' | 'where'>
Pick<
JoinField,
'collection' | 'defaultLimit' | 'defaultSort' | 'index' | 'maxDepth' | 'on' | 'type' | 'where'
>
export type FlattenedBlock = {
flattenedFields: FlattenedField[]

View File

@@ -350,10 +350,13 @@ export const promise = async ({
case 'array': {
const rows = siblingDoc[field.name] as JsonObject
const arraySelect = select?.[field.name]
let arraySelect = select?.[field.name]
if (selectMode === 'include' && typeof arraySelect === 'object') {
arraySelect.id = true
arraySelect = {
...arraySelect,
id: true,
}
}
if (Array.isArray(rows)) {
@@ -427,7 +430,7 @@ export const promise = async ({
case 'blocks': {
const rows = siblingDoc[field.name]
const blocksSelect = select?.[field.name]
let blocksSelect = select?.[field.name]
if (Array.isArray(rows)) {
rows.forEach((row, i) => {
@@ -438,6 +441,10 @@ export const promise = async ({
let blockSelectMode = selectMode
if (typeof blocksSelect === 'object') {
blocksSelect = {
...blocksSelect,
}
// sanitize blocks: {cta: false} to blocks: {cta: {id: true, blockType: true}}
if (selectMode === 'exclude' && blocksSelect[block.slug] === false) {
blockSelectMode = 'include'
@@ -451,6 +458,10 @@ export const promise = async ({
}
if (typeof blocksSelect[block.slug] === 'object') {
blocksSelect[block.slug] = {
...(blocksSelect[block.slug] as object),
}
blocksSelect[block.slug]['id'] = true
blocksSelect[block.slug]['blockType'] = true
}
@@ -535,7 +546,6 @@ export const promise = async ({
}
case 'collapsible':
case 'row': {
traverseFields({
collection,

View File

@@ -102,6 +102,10 @@ export const createClientGlobalConfig = ({
importMap,
})
break
case 'label':
clientGlobal.label =
typeof global.label === 'function' ? global.label({ t: i18n.t }) : global.label
break
default: {
clientGlobal[key] = global[key]
break

View File

@@ -14,8 +14,10 @@ import type {
EntityDescription,
EntityDescriptionComponent,
GeneratePreviewURL,
LabelFunction,
LivePreviewConfig,
MetaConfig,
StaticLabel,
} from '../../config/types.js'
import type { DBIdentifierName } from '../../database/types.js'
import type { Field, FlattenedField } from '../../fields/config/types.js'
@@ -165,7 +167,7 @@ export type GlobalConfig = {
beforeRead?: BeforeReadHook[]
beforeValidate?: BeforeValidateHook[]
}
label?: Record<string, string> | string
label?: LabelFunction | StaticLabel
/**
* Enables / Disables the ability to lock documents while editing
* @default true

View File

@@ -807,7 +807,7 @@ export const getPayload = async (
// will reach `if (cached.reload instanceof Promise) {` which then waits for the first reload to finish.
cached.reload = new Promise((res) => (resolve = res))
const config = await options.config
await reload(config, cached.payload)
await reload(config, cached.payload, !options.importMap)
resolve()
}
@@ -815,7 +815,6 @@ export const getPayload = async (
if (cached.reload instanceof Promise) {
await cached.reload
}
if (options?.importMap) {
cached.payload.importMap = options.importMap
}
@@ -839,6 +838,7 @@ export const getPayload = async (
) {
try {
const port = process.env.PORT || '3000'
cached.ws = new WebSocket(
`ws://localhost:${port}${process.env.NEXT_BASE_PATH ?? ''}/_next/webpack-hmr`,
)

View File

@@ -134,14 +134,14 @@ export const i18n: Partial<GenericLanguages> = {
alignRightLabel: 'Aliniați la dreapta',
},
rs: {
alignCenterLabel: 'Centriraj',
alignJustifyLabel: 'Poravnaj opravdaj',
alignLeftLabel: 'Poravnaj levo',
alignRightLabel: 'Poravnaj desno',
alignCenterLabel: 'Поравнај по средини',
alignJustifyLabel: 'Поравнај обострано',
alignLeftLabel: 'Поравнај лево',
alignRightLabel: 'Поравнај десно',
},
'rs-latin': {
alignCenterLabel: 'Poravnaj centar',
alignJustifyLabel: 'Poravnaj opravdanje',
alignCenterLabel: 'Poravnaj po sredini',
alignJustifyLabel: 'Poravnaj obostrano',
alignLeftLabel: 'Poravnaj levo',
alignRightLabel: 'Poravnaj desno',
},

View File

@@ -68,7 +68,7 @@ export const i18n: Partial<GenericLanguages> = {
label: 'Citat',
},
rs: {
label: 'Blok citat',
label: 'Блок цитата',
},
'rs-latin': {
label: 'Blok citata',

View File

@@ -201,18 +201,18 @@ export const i18n: Partial<GenericLanguages> = {
},
rs: {
inlineBlocks: {
create: 'Kreiraj {{label}}',
edit: 'Izmeni {{label}}',
label: 'Umetnuti blokovi',
remove: 'Ukloni {{label}}',
create: 'Креирај {{label}}',
edit: 'Измени {{label}}',
label: 'Уметнути блокови',
remove: 'Уклони {{label}}',
},
label: 'Blokovi',
label: 'Блокови',
},
'rs-latin': {
inlineBlocks: {
create: 'Kreiraj {{label}}',
edit: 'Izmeni {{label}}',
label: 'Unutar blokovi',
label: 'Umetnuti blokovi',
remove: 'Ukloni {{oznaka}}',
},
label: 'Blokovi',

View File

@@ -68,7 +68,7 @@ export const i18n: Partial<GenericLanguages> = {
label: 'Titlu {{headingLevel}}',
},
rs: {
label: 'Naslov {{headingLevel}}',
label: 'Наслов {{headingLevel}}',
},
'rs-latin': {
label: 'Naslov {{headingLevel}}',

View File

@@ -68,7 +68,7 @@ export const i18n: Partial<GenericLanguages> = {
label: 'Linie orizontală',
},
rs: {
label: 'Horizontalna linija',
label: 'Хоризонтална линија',
},
'rs-latin': {
label: 'Horizontalna linija',

View File

@@ -90,8 +90,8 @@ export const i18n: Partial<GenericLanguages> = {
increaseLabel: 'Crește indentarea',
},
rs: {
decreaseLabel: 'Smanji uvlačenje',
increaseLabel: 'Povećaj uvlačenje',
decreaseLabel: 'Смањи увлачење',
increaseLabel: 'Повећај увлачење',
},
'rs-latin': {
decreaseLabel: 'Smanji uvlačenje',

View File

@@ -90,7 +90,7 @@ export const i18n: Partial<GenericLanguages> = {
loadingWithEllipsis: 'Se încarcă...',
},
rs: {
label: 'Veza',
label: 'Веза',
loadingWithEllipsis: 'Учитавање...',
},
'rs-latin': {

View File

@@ -68,10 +68,10 @@ export const i18n: Partial<GenericLanguages> = {
label: 'Listă de verificare',
},
rs: {
label: 'Lista provere',
label: 'Контролна листа',
},
'rs-latin': {
label: 'Lista provere',
label: 'Kontrolna lista',
},
ru: {
label: 'Список Проверки',

View File

@@ -68,10 +68,10 @@ export const i18n: Partial<GenericLanguages> = {
label: 'Lista ordonată',
},
rs: {
label: 'Naručeni Spisak',
label: 'Уређена листа',
},
'rs-latin': {
label: 'Naručeni spisak',
label: 'Uređena lista',
},
ru: {
label: 'Упорядоченный список',

View File

@@ -68,10 +68,10 @@ export const i18n: Partial<GenericLanguages> = {
label: 'Listă neordonată',
},
rs: {
label: 'Neporedani spisak',
label: 'Неуређена листа',
},
'rs-latin': {
label: 'Neuređena Lista',
label: 'Neuređena lista',
},
ru: {
label: 'Несортированный список',

View File

@@ -90,12 +90,12 @@ export const i18n: Partial<GenericLanguages> = {
label2: 'Text normal',
},
rs: {
label: 'Paragraf',
label2: 'Normalan tekst',
label: 'Параграф',
label2: 'Oбичан текст',
},
'rs-latin': {
label: 'Paragraf',
label2: 'Normalan tekst',
label2: 'Običan tekst',
},
ru: {
label: 'Параграф',

View File

@@ -68,10 +68,10 @@ export const i18n: Partial<GenericLanguages> = {
label: 'Relație',
},
rs: {
label: 'Veza',
label: 'Релација',
},
'rs-latin': {
label: 'Odnos',
label: 'Relacija',
},
ru: {
label: 'Отношения',

View File

@@ -68,7 +68,7 @@ export const i18n: Partial<GenericLanguages> = {
label: 'Încarcă',
},
rs: {
label: 'Otpremi',
label: 'Отпреми',
},
'rs-latin': {
label: 'Otpremi',

View File

@@ -134,10 +134,10 @@ export const i18n: Partial<GenericLanguages> = {
toolbarItemsActive: '{{count}} activ',
},
rs: {
placeholder: "Počnite da kucate, ili pritisnite '/' za komande...",
slashMenuBasicGroupLabel: 'Osnovno',
slashMenuListGroupLabel: 'Liste',
toolbarItemsActive: '{{count}} aktivno',
placeholder: "Почните да куцате, или притисните '/' за команде...",
slashMenuBasicGroupLabel: 'Основно',
slashMenuListGroupLabel: 'Листе',
toolbarItemsActive: '{{count}} активно',
},
'rs-latin': {
placeholder: "Počnite da kucate, ili pritisnite '/' za komande...",

View File

@@ -183,6 +183,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, Props>((
button={<ChevronIcon />}
buttonSize={size}
className={disabled ? `${baseClass}--popup-disabled` : ''}
disabled={disabled}
horizontalAlign="right"
noBackground
render={({ close }) => SubMenuPopupContent({ close: () => close() })}

View File

@@ -27,5 +27,9 @@
&--size-large {
padding: base(0.8);
}
&--disabled {
cursor: not-allowed;
}
}
}

View File

@@ -25,6 +25,7 @@ export const PopupTrigger: React.FC<PopupTriggerProps> = (props) => {
`${baseClass}--${buttonType}`,
!noBackground && `${baseClass}--background`,
size && `${baseClass}--size-${size}`,
disabled && `${baseClass}--disabled`,
]
.filter(Boolean)
.join(' ')

View File

@@ -31,7 +31,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
const { submit } = useForm()
const modified = useFormModified()
const editDepth = useEditDepth()
const { code: locale } = useLocale()
const { code: localeCode } = useLocale()
const {
localization,
@@ -40,7 +40,6 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
} = config
const { i18n, t } = useTranslation()
const { code } = useLocale()
const label = labelProp || t('version:publishChanges')
const hasNewerVersions = unpublishedVersionCount > 0
@@ -54,7 +53,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
return
}
const search = `?locale=${locale}&depth=0&fallback-locale=null&draft=true`
const search = `?locale=${localeCode}&depth=0&fallback-locale=null&draft=true`
let action
let method = 'POST'
@@ -77,7 +76,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
},
skipValidation: true,
})
}, [submit, collectionSlug, globalSlug, serverURL, api, locale, id, forceDisable])
}, [submit, collectionSlug, globalSlug, serverURL, api, localeCode, id, forceDisable])
useHotkey({ cmdCtrlKey: true, editDepth, keyCodes: ['s'] }, (e) => {
e.preventDefault()
@@ -140,7 +139,8 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
? locale.label
: locale.label && locale.label[i18n?.language]
const isActive = typeof locale === 'string' ? locale === code : locale.code === code
const isActive =
typeof locale === 'string' ? locale === localeCode : locale.code === localeCode
if (isActive) {
return (

View File

@@ -53,6 +53,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
allowCreate = true,
BeforeInput,
disableTable = false,
field,
filterOptions,
initialData: initialDataFromProps,
initialDrawerData,
@@ -104,6 +105,8 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
const renderTable = useCallback(
async (docs?: PaginatedDocs['docs']) => {
const newQuery: ListQuery = {
limit: String(field.defaultLimit || collectionConfig.admin.pagination.defaultLimit),
sort: field.defaultSort || collectionConfig.defaultSort,
...(query || {}),
where: { ...(query?.where || {}) },
}
@@ -130,7 +133,16 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
setColumnState(newColumnState)
setIsLoadingTable(false)
},
[getTableState, relationTo, filterOptions, query],
[
query,
field.defaultLimit,
field.defaultSort,
collectionConfig.admin.pagination.defaultLimit,
collectionConfig.defaultSort,
filterOptions,
getTableState,
relationTo,
],
)
useIgnoredEffect(
@@ -227,7 +239,9 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
<ListQueryProvider
collectionSlug={relationTo}
data={data}
defaultLimit={collectionConfig?.admin?.pagination?.defaultLimit}
defaultLimit={
field.defaultLimit ?? collectionConfig?.admin?.pagination?.defaultLimit
}
modifySearchParams={false}
onQueryChange={setQuery}
preferenceKey={preferenceKey}

View File

@@ -71,31 +71,33 @@ export const GroupFieldComponent: GroupFieldClientComponent = (props) => {
>
<GroupProvider>
<div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__header`}>
{Boolean(Label || Description || label) && (
<header>
<RenderCustomComponent
CustomComponent={Label}
Fallback={
<h3 className={`${baseClass}__title`}>
<FieldLabel
as="span"
label={getTranslation(label, i18n)}
localized={false}
path={path}
required={false}
/>
</h3>
}
/>
<RenderCustomComponent
CustomComponent={Description}
Fallback={<FieldDescription description={description} path={path} />}
/>
</header>
)}
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
</div>
{Boolean(Label || Description || label || fieldHasErrors) && (
<div className={`${baseClass}__header`}>
{Boolean(Label || Description || label) && (
<header>
<RenderCustomComponent
CustomComponent={Label}
Fallback={
<h3 className={`${baseClass}__title`}>
<FieldLabel
as="span"
label={getTranslation(label, i18n)}
localized={false}
path={path}
required={false}
/>
</h3>
}
/>
<RenderCustomComponent
CustomComponent={Description}
Fallback={<FieldDescription description={description} path={path} />}
/>
</header>
)}
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
</div>
)}
{BeforeInput}
<RenderFields
fields={fields}

View File

@@ -27,11 +27,11 @@ import { useTranslation } from '../../providers/Translation/index.js'
import { mergeFieldStyles } from '../mergeFieldStyles.js'
import { fieldBaseClass } from '../shared/index.js'
import { createRelationMap } from './createRelationMap.js'
import './index.scss'
import { findOptionsByValue } from './findOptionsByValue.js'
import { optionsReducer } from './optionsReducer.js'
import { MultiValueLabel } from './select-components/MultiValueLabel/index.js'
import { SingleValue } from './select-components/SingleValue/index.js'
import './index.scss'
const maxResultsPerRequest = 10
@@ -310,7 +310,6 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
// ///////////////////////////////////
// Ensure we have an option for each value
// ///////////////////////////////////
useIgnoredEffect(
() => {
const relationMap = createRelationMap({

View File

@@ -30,6 +30,7 @@ export type SelectInputProps = {
readonly localized?: boolean
readonly name: string
readonly onChange?: ReactSelectAdapterProps['onChange']
readonly onInputChange?: ReactSelectAdapterProps['onInputChange']
readonly options?: OptionObject[]
readonly path: string
readonly readOnly?: boolean
@@ -54,6 +55,7 @@ export const SelectInput: React.FC<SelectInputProps> = (props) => {
Label,
localized,
onChange,
onInputChange,
options,
path,
readOnly,
@@ -115,6 +117,7 @@ export const SelectInput: React.FC<SelectInputProps> = (props) => {
isMulti={hasMany}
isSortable={isSortable}
onChange={onChange}
onInputChange={onInputChange}
options={options.map((option) => ({
...option,
label: getTranslation(option.label, i18n),

View File

@@ -485,6 +485,7 @@ export const Form: React.FC<FormProps> = (props) => {
docPermissions,
docPreferences,
globalSlug,
locale,
operation,
renderAllFields: true,
schemaPath: collectionSlug ? collectionSlug : globalSlug,
@@ -504,6 +505,7 @@ export const Form: React.FC<FormProps> = (props) => {
getFormState,
docPermissions,
getDocPreferences,
locale,
],
)

View File

@@ -1,5 +1,5 @@
'use client'
import type { ListQuery, PaginatedDocs, Where } from 'payload'
import type { ListQuery, PaginatedDocs, Sort, Where } from 'payload'
import { useRouter, useSearchParams } from 'next/navigation.js'
import { isNumber } from 'payload/shared'
@@ -27,7 +27,7 @@ export type ListQueryProps = {
readonly collectionSlug: string
readonly data: PaginatedDocs
readonly defaultLimit?: number
readonly defaultSort?: string
readonly defaultSort?: Sort
readonly modifySearchParams?: boolean
readonly onQueryChange?: (query: ListQuery) => void
readonly preferenceKey?: string
@@ -36,7 +36,7 @@ export type ListQueryProps = {
export type ListQueryContext = {
data: PaginatedDocs
defaultLimit?: number
defaultSort?: string
defaultSort?: Sort
query: ListQuery
refineListData: (args: ListQuery) => Promise<void>
} & ContextHandlers
@@ -103,10 +103,13 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
}
const newQuery: ListQuery = {
limit: 'limit' in query ? query.limit : (currentQuery?.limit as string),
limit:
'limit' in query
? query.limit
: ((currentQuery?.limit as string) ?? String(defaultLimit)),
page: pageQuery as string,
search: 'search' in query ? query.search : (currentQuery?.search as string),
sort: 'sort' in query ? query.sort : (currentQuery?.sort as string),
sort: 'sort' in query ? query.sort : ((currentQuery?.sort as string) ?? defaultSort),
where: 'where' in query ? query.where : (currentQuery?.where as Where),
}

View File

@@ -21,73 +21,64 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
const defaultLocale =
localization && localization.defaultLocale ? localization.defaultLocale : 'en'
const { getPreference, setPreference } = usePreferences()
const searchParams = useSearchParams()
const localeFromParams = searchParams.get('locale')
const [localeCode, setLocaleCode] = useState<string>(localeFromParams || defaultLocale)
const [localeCode, setLocaleCode] = useState<string>(defaultLocale)
const [locale, setLocale] = useState<Locale | null>(
localization && findLocaleFromCode(localization, localeCode),
)
const locale: Locale = React.useMemo(() => {
if (!localization) {
// TODO: return null V4
return {} as Locale
}
const { getPreference, setPreference } = usePreferences()
const switchLocale = React.useCallback(
async (newLocale: string) => {
if (!localization) {
return
}
const localeToSet =
localization.localeCodes.indexOf(newLocale) > -1 ? newLocale : defaultLocale
if (localeToSet !== localeCode) {
setLocaleCode(localeToSet)
setLocale(findLocaleFromCode(localization, localeToSet))
try {
if (user) {
await setPreference('locale', localeToSet)
}
} catch (error) {
// swallow error
}
}
},
[localization, setPreference, user, defaultLocale, localeCode],
)
return (
findLocaleFromCode(localization, localeFromParams || localeCode) ||
findLocaleFromCode(localization, defaultLocale)
)
}, [localeCode, localeFromParams, localization, defaultLocale])
useEffect(() => {
async function setInitialLocale() {
let localeToSet = defaultLocale
if (typeof localeFromParams === 'string') {
localeToSet = localeFromParams
} else if (user) {
try {
localeToSet = await getPreference<string>('locale')
} catch (error) {
// swallow error
if (localization && user) {
if (typeof localeFromParams !== 'string') {
try {
const localeToSet = await getPreference<string>('locale')
setLocaleCode(localeToSet)
} catch (_) {
setLocaleCode(defaultLocale)
}
} else {
void setPreference(
'locale',
findLocaleFromCode(localization, localeFromParams)?.code || defaultLocale,
)
}
}
await switchLocale(localeToSet)
}
void setInitialLocale()
}, [
defaultLocale,
getPreference,
localization,
localeFromParams,
setPreference,
user,
switchLocale,
])
}, [defaultLocale, getPreference, localization, localeFromParams, setPreference, user])
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
}
/**
* A hook that returns the current locale object.
* @deprecated A hook that returns the current locale object.
*
* ---
*
* #### 🚨 V4 Breaking Change
* The `useLocale` return type now reflects `null | Locale` instead of `false | Locale`.
*
* **Old (V3):**
* ```ts
* const { code } = useLocale();
* ```
* **New (V4):**
* ```ts
* const locale = useLocale();
* ```
*/
export const useLocale = (): Locale => useContext(LocaleContext)

View File

@@ -4,8 +4,6 @@ import { useSearchParams as useNextSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm'
import React, { createContext, useContext } from 'react'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
export type SearchParamsContext = {
searchParams: qs.ParsedQs
stringifyParams: ({ params, replace }: { params: qs.ParsedQs; replace?: boolean }) => string
@@ -28,8 +26,16 @@ const Context = createContext(initialContext)
*/
export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const nextSearchParams = useNextSearchParams()
const searchString = nextSearchParams.toString()
const [searchParams, setSearchParams] = React.useState(() => parseSearchParams(nextSearchParams))
const searchParams = React.useMemo(
() =>
qs.parse(searchString, {
depth: 10,
ignoreQueryPrefix: true,
}),
[searchString],
)
const stringifyParams = React.useCallback(
({ params, replace = false }: { params: qs.ParsedQs; replace?: boolean }) => {
@@ -44,10 +50,6 @@ export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({
[searchParams],
)
React.useEffect(() => {
setSearchParams(parseSearchParams(nextSearchParams))
}, [nextSearchParams])
return <Context.Provider value={{ searchParams, stringifyParams }}>{children}</Context.Provider>
}

View File

@@ -42,6 +42,14 @@ export function groupNavItems(
if (permissions?.[entityToGroup.type.toLowerCase()]?.[entityToGroup.entity.slug]?.read) {
const translatedGroup = getTranslation(entityToGroup.entity.admin.group, i18n)
const labelOrFunction =
'labels' in entityToGroup.entity
? entityToGroup.entity.labels.plural
: entityToGroup.entity.label
const label =
typeof labelOrFunction === 'function' ? labelOrFunction({ t: i18n.t }) : labelOrFunction
if (entityToGroup.entity.admin.group) {
const existingGroup = groups.find(
(group) => getTranslation(group.label, i18n) === translatedGroup,
@@ -57,12 +65,7 @@ export function groupNavItems(
matchedGroup.entities.push({
slug: entityToGroup.entity.slug,
type: entityToGroup.type,
label:
'labels' in entityToGroup.entity
? typeof entityToGroup.entity.labels.plural === 'function'
? entityToGroup.entity.labels.plural({ t: i18n.t })
: entityToGroup.entity.labels.plural
: entityToGroup.entity.label,
label,
})
} else {
const defaultGroup = groups.find((group) => {
@@ -71,12 +74,7 @@ export function groupNavItems(
defaultGroup.entities.push({
slug: entityToGroup.entity.slug,
type: entityToGroup.type,
label:
'labels' in entityToGroup.entity
? typeof entityToGroup.entity.labels.plural === 'function'
? entityToGroup.entity.labels.plural({ t: i18n.t })
: entityToGroup.entity.labels.plural
: entityToGroup.entity.label,
label,
})
}
}

16
pnpm-lock.yaml generated
View File

@@ -269,8 +269,8 @@ importers:
specifier: 1.6.2
version: 1.6.2
mongoose:
specifier: 8.8.1
version: 8.8.1(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
specifier: 8.8.3
version: 8.8.3(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
mongoose-aggregate-paginate-v2:
specifier: 1.1.2
version: 1.1.2
@@ -1745,8 +1745,8 @@ importers:
specifier: 4.0.0
version: 4.0.0
mongoose:
specifier: 8.8.1
version: 8.8.1(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
specifier: 8.8.3
version: 8.8.3(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
next:
specifier: 15.0.2
version: 15.0.2(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.77.4)
@@ -8272,8 +8272,8 @@ packages:
resolution: {integrity: sha512-kFxhot+yw9KmpAGSSrF/o+f00aC2uawgNUbhyaM0USS9L7dln1NA77/pLg4lgOaRgXMtfgCENamjqZwIM1Zrig==}
engines: {node: '>=4.0.0'}
mongoose@8.8.1:
resolution: {integrity: sha512-l7DgeY1szT98+EKU8GYnga5WnyatAu+kOQ2VlVX1Mxif6A0Umt0YkSiksCiyGxzx8SPhGe9a53ND1GD4yVDrPA==}
mongoose@8.8.3:
resolution: {integrity: sha512-/I4n/DcXqXyIiLRfAmUIiTjj3vXfeISke8dt4U4Y8Wfm074Wa6sXnQrXN49NFOFf2mM1kUdOXryoBvkuCnr+Qw==}
engines: {node: '>=16.20.1'}
mpath@0.9.0:
@@ -14568,7 +14568,7 @@ snapshots:
'@types/mongoose-aggregate-paginate-v2@1.0.12(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)':
dependencies:
'@types/node': 22.5.4
mongoose: 8.8.1(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
mongoose: 8.8.3(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
@@ -18416,7 +18416,7 @@ snapshots:
mongoose-paginate-v2@1.8.5: {}
mongoose@8.8.1(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3):
mongoose@8.8.3(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3):
dependencies:
bson: 6.9.0
kareem: 2.6.3

View File

@@ -112,7 +112,7 @@ export const generateReleaseNotes = async (args: Args = {}): Promise<ChangelogRe
return sections
},
{} as Record<Sections | 'breaking', GitCommit[]>,
{} as Record<'breaking' | Sections, GitCommit[]>,
)
// Sort commits by scope, unscoped first

View File

@@ -1,14 +1,20 @@
/** @type {import('next-sitemap').IConfig} */
const SITE_URL =
process.env.NEXT_PUBLIC_SERVER_URL ||
process.env.VERCEL_PROJECT_PRODUCTION_URL ||
'https://example.com'
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: SITE_URL,
generateRobotsTxt: true,
exclude: ['/posts-sitemap.xml', '/pages-sitemap.xml', '/*', '/posts/*'],
robotsTxtOptions: {
policies: [
{
userAgent: '*',
disallow: '/admin/*',
},
],
additionalSitemaps: [`${SITE_URL}/pages-sitemap.xml`, `${SITE_URL}/posts-sitemap.xml`],
},
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
'use client'
import { Header } from '@/payload-types'
import { RowLabelProps, useRowLabel } from '@payloadcms/ui'
export const RowLabel: React.FC<RowLabelProps> = (props) => {
const data = useRowLabel<NonNullable<Header['navItems']>[number]>()
const label = data?.data?.link?.label
? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}`
: 'Row'
return <div>{label}</div>
}

View File

@@ -18,6 +18,12 @@ export const Footer: GlobalConfig = {
}),
],
maxRows: 6,
admin: {
initCollapsed: true,
components: {
RowLabel: '@/Footer/RowLabel#RowLabel',
},
},
},
],
hooks: {

View File

@@ -2,10 +2,12 @@ import type { GlobalAfterChangeHook } from 'payload'
import { revalidateTag } from 'next/cache'
export const revalidateFooter: GlobalAfterChangeHook = ({ doc, req: { payload } }) => {
payload.logger.info(`Revalidating footer`)
export const revalidateFooter: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => {
if (!context.disableRevalidate) {
payload.logger.info(`Revalidating footer`)
revalidateTag('global_footer')
revalidateTag('global_footer')
}
return doc
}

View File

@@ -0,0 +1,13 @@
'use client'
import { Header } from '@/payload-types'
import { RowLabelProps, useRowLabel } from '@payloadcms/ui'
export const RowLabel: React.FC<RowLabelProps> = (props) => {
const data = useRowLabel<NonNullable<Header['navItems']>[number]>()
const label = data?.data?.link?.label
? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}`
: 'Row'
return <div>{label}</div>
}

View File

@@ -18,6 +18,12 @@ export const Header: GlobalConfig = {
}),
],
maxRows: 6,
admin: {
initCollapsed: true,
components: {
RowLabel: '@/Header/RowLabel#RowLabel',
},
},
},
],
hooks: {

View File

@@ -2,10 +2,12 @@ import type { GlobalAfterChangeHook } from 'payload'
import { revalidateTag } from 'next/cache'
export const revalidateHeader: GlobalAfterChangeHook = ({ doc, req: { payload } }) => {
payload.logger.info(`Revalidating header`)
export const revalidateHeader: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => {
if (!context.disableRevalidate) {
payload.logger.info(`Revalidating header`)
revalidateTag('global_header')
revalidateTag('global_header')
}
return doc
}

View File

@@ -3,7 +3,6 @@ import { seed } from '@/endpoints/seed'
import config from '@payload-config'
import { headers } from 'next/headers'
const payloadToken = 'payload-token'
export const maxDuration = 60 // This function can run for a maximum of 60 seconds
export async function POST(

View File

@@ -58,7 +58,7 @@ export default async function Post({ params: paramsPromise }: Args) {
<div className="flex flex-col items-center gap-4 pt-8">
<div className="container">
<RichText className="max-w-[48rem] mx-auto" content={post.content} enableGutter={false} />
<RichText className="max-w-[48rem] mx-auto" data={post.content} enableGutter={false} />
{post.relatedPosts && post.relatedPosts.length > 0 && (
<RelatedPosts
className="mt-12 max-w-[52rem] lg:grid lg:grid-cols-subgrid col-start-1 col-span-3 grid-rows-[2fr]"

View File

@@ -18,6 +18,8 @@ import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkToDoc as LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
import { ReindexButton as ReindexButton_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
import { RowLabel as RowLabel_ec255a65fa6fa8d1faeb09cf35284224 } from '@/Header/RowLabel'
import { RowLabel as RowLabel_1f6ff6ff633e3695d348f4f3c58f1466 } from '@/Footer/RowLabel'
import { default as default_1a7510af427896d367a49dbf838d2de6 } from '@/components/BeforeDashboard'
import { default as default_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
@@ -59,6 +61,8 @@ export const importMap = {
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/plugin-search/client#LinkToDoc': LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
'@payloadcms/plugin-search/client#ReindexButton': ReindexButton_aead06e4cbf6b2620c5c51c9ab283634,
'@/Header/RowLabel#RowLabel': RowLabel_ec255a65fa6fa8d1faeb09cf35284224,
'@/Footer/RowLabel#RowLabel': RowLabel_1f6ff6ff633e3695d348f4f3c58f1466,
'@/components/BeforeDashboard#default': default_1a7510af427896d367a49dbf838d2de6,
'@/components/BeforeLogin#default': default_8a7ab0eb7ab5c511aba12e68480bfe5e,
}

View File

@@ -56,7 +56,7 @@ export const ArchiveBlock: React.FC<
<div className="my-16" id={`block-${id}`}>
{introContent && (
<div className="container mb-16">
<RichText className="ml-0 max-w-[48rem]" content={introContent} enableGutter={false} />
<RichText className="ml-0 max-w-[48rem]" data={introContent} enableGutter={false} />
</div>
)}
<CollectionArchive posts={posts} />

View File

@@ -19,7 +19,7 @@ export const BannerBlock: React.FC<Props> = ({ className, content, style }) => {
'border-warning bg-warning/30': style === 'warning',
})}
>
<RichText content={content} enableGutter={false} enableProse={false} />
<RichText data={content} enableGutter={false} enableProse={false} />
</div>
</div>
)

View File

@@ -10,7 +10,7 @@ export const CallToActionBlock: React.FC<CTABlockProps> = ({ links, richText })
<div className="container">
<div className="bg-card rounded border-border border p-4 flex flex-col gap-8 md:flex-row md:justify-between md:items-center">
<div className="max-w-[48rem] flex items-center">
{richText && <RichText className="mb-0" content={richText} enableGutter={false} />}
{richText && <RichText className="mb-0" data={richText} enableGutter={false} />}
</div>
<div className="flex flex-col gap-8">
{(links || []).map(({ link }, i) => {

View File

@@ -31,7 +31,7 @@ export const ContentBlock: React.FC<ContentBlockProps> = (props) => {
})}
key={index}
>
{richText && <RichText content={richText} enableGutter={false} />}
{richText && <RichText data={richText} enableGutter={false} />}
{enableLink && <CMSLink {...link} />}
</div>

View File

@@ -68,6 +68,9 @@ export const Content: Block = {
{
name: 'columns',
type: 'array',
admin: {
initCollapsed: true,
},
fields: columnFields,
},
],

View File

@@ -6,6 +6,7 @@ import React, { useCallback, useState } from 'react'
import { useForm, FormProvider } from 'react-hook-form'
import RichText from '@/components/RichText'
import { Button } from '@/components/ui/button'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { buildInitialFormState } from './buildInitialFormState'
import { fields } from './fields'
@@ -26,9 +27,7 @@ export type FormBlockType = {
blockType?: 'formBlock'
enableIntro: boolean
form: FormType
introContent?: {
[k: string]: unknown
}[]
introContent?: SerializedEditorState
}
export const FormBlock: React.FC<
@@ -128,12 +127,12 @@ export const FormBlock: React.FC<
return (
<div className="container lg:max-w-[48rem]">
{enableIntro && introContent && !hasSubmitted && (
<RichText className="mb-8 lg:mb-12" content={introContent} enableGutter={false} />
<RichText className="mb-8 lg:mb-12" data={introContent} enableGutter={false} />
)}
<div className="p-4 lg:p-6 border border-border rounded-[0.8rem]">
<FormProvider {...formMethods}>
{!isLoading && hasSubmitted && confirmationType === 'message' && (
<RichText content={confirmationMessage} />
<RichText data={confirmationMessage} />
)}
{isLoading && !hasSubmitted && <p>Loading, please wait...</p>}
{error && <div>{`${error.status || '500'}: ${error.message || ''}`}</div>}

View File

@@ -2,11 +2,12 @@ import RichText from '@/components/RichText'
import React from 'react'
import { Width } from '../Width'
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
export const Message: React.FC = ({ message }: { message: Record<string, any> }) => {
export const Message: React.FC = ({ message }: { message: SerializedEditorState }) => {
return (
<Width className="my-12" width="100">
{message && <RichText content={message} />}
{message && <RichText data={message} />}
</Width>
)
}

View File

@@ -57,7 +57,7 @@ export const MediaBlock: React.FC<Props> = (props) => {
captionClassName,
)}
>
<RichText content={caption} enableGutter={false} />
<RichText data={caption} enableGutter={false} />
</div>
)}
</div>

View File

@@ -16,8 +16,8 @@ export const RelatedPosts: React.FC<RelatedPostsProps> = (props) => {
const { className, docs, introContent } = props
return (
<div className={clsx('container', className)}>
{introContent && <RichText content={introContent} enableGutter={false} />}
<div className={clsx('lg:container', className)}>
{introContent && <RichText data={introContent} enableGutter={false} />}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8 items-stretch">
{docs?.map((doc, index) => {

View File

@@ -7,34 +7,37 @@ import type { Page } from '../../../payload-types'
export const revalidatePage: CollectionAfterChangeHook<Page> = ({
doc,
previousDoc,
req: { payload },
req: { payload, context },
}) => {
if (doc._status === 'published') {
const path = doc.slug === 'home' ? '/' : `/${doc.slug}`
if (!context.disableRevalidate) {
if (doc._status === 'published') {
const path = doc.slug === 'home' ? '/' : `/${doc.slug}`
payload.logger.info(`Revalidating page at path: ${path}`)
payload.logger.info(`Revalidating page at path: ${path}`)
revalidatePath(path)
revalidateTag('pages-sitemap')
}
// If the page was previously published, we need to revalidate the old path
if (previousDoc?._status === 'published' && doc._status !== 'published') {
const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}`
payload.logger.info(`Revalidating old page at path: ${oldPath}`)
revalidatePath(oldPath)
revalidateTag('pages-sitemap')
}
}
return doc
}
export const revalidateDelete: CollectionAfterDeleteHook<Page> = ({ doc, req: { context } }) => {
if (!context.disableRevalidate) {
const path = doc?.slug === 'home' ? '/' : `/${doc?.slug}`
revalidatePath(path)
revalidateTag('pages-sitemap')
}
// If the page was previously published, we need to revalidate the old path
if (previousDoc?._status === 'published' && doc._status !== 'published') {
const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}`
payload.logger.info(`Revalidating old page at path: ${oldPath}`)
revalidatePath(oldPath)
revalidateTag('pages-sitemap')
}
return doc
}
export const revalidateDelete: CollectionAfterDeleteHook<Page> = ({ doc }) => {
const path = doc?.slug === 'home' ? '/' : `/${doc?.slug}`
revalidatePath(path)
revalidateTag('pages-sitemap')
return doc
}

View File

@@ -79,6 +79,9 @@ export const Pages: CollectionConfig<'pages'> = {
type: 'blocks',
blocks: [CallToAction, Content, MediaBlock, Archive, FormBlock],
required: true,
admin: {
initCollapsed: true,
},
},
],
label: 'Content',

View File

@@ -7,35 +7,38 @@ import type { Post } from '../../../payload-types'
export const revalidatePost: CollectionAfterChangeHook<Post> = ({
doc,
previousDoc,
req: { payload },
req: { payload, context },
}) => {
if (doc._status === 'published') {
const path = `/posts/${doc.slug}`
if (!context.disableRevalidate) {
if (doc._status === 'published') {
const path = `/posts/${doc.slug}`
payload.logger.info(`Revalidating post at path: ${path}`)
payload.logger.info(`Revalidating post at path: ${path}`)
revalidatePath(path)
revalidateTag('posts-sitemap')
}
// If the post was previously published, we need to revalidate the old path
if (previousDoc._status === 'published' && doc._status !== 'published') {
const oldPath = `/posts/${previousDoc.slug}`
payload.logger.info(`Revalidating old post at path: ${oldPath}`)
revalidatePath(oldPath)
revalidateTag('posts-sitemap')
}
}
return doc
}
export const revalidateDelete: CollectionAfterDeleteHook<Post> = ({ doc, req: { context } }) => {
if (!context.disableRevalidate) {
const path = `/posts/${doc?.slug}`
revalidatePath(path)
revalidateTag('posts-sitemap')
}
// If the post was previously published, we need to revalidate the old path
if (previousDoc._status === 'published' && doc._status !== 'published') {
const oldPath = `/posts/${previousDoc.slug}`
payload.logger.info(`Revalidating old post at path: ${oldPath}`)
revalidatePath(oldPath)
revalidateTag('posts-sitemap')
}
return doc
}
export const revalidateDelete: CollectionAfterDeleteHook<Post> = ({ doc }) => {
const path = `/posts/${doc?.slug}`
revalidatePath(path)
revalidateTag('posts-sitemap')
return doc
}

View File

@@ -1,43 +1,65 @@
import { cn } from '@/utilities/cn'
import React from 'react'
import { MediaBlock } from '@/blocks/MediaBlock/Component'
import { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical'
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
JSXConvertersFunction,
RichText as RichTextWithoutBlocks,
} from '@payloadcms/richtext-lexical/react'
import { serializeLexical } from './serialize'
import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component'
import type {
BannerBlock as BannerBlockProps,
CallToActionBlock as CTABlockProps,
MediaBlock as MediaBlockProps,
} from '@/payload-types'
import { BannerBlock } from '@/blocks/Banner/Component'
import { CallToActionBlock } from '@/blocks/CallToAction/Component'
import { cn } from '@/utilities/cn'
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<CTABlockProps | MediaBlockProps | BannerBlockProps | CodeBlockProps>
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
blocks: {
banner: ({ node }) => <BannerBlock className="col-start-2 mb-4" {...node.fields} />,
mediaBlock: ({ node }) => (
<MediaBlock
className="col-start-1 col-span-3"
imgClassName="m-0"
{...node.fields}
captionClassName="mx-auto max-w-[48rem]"
enableGutter={false}
disableInnerContainer={true}
/>
),
code: ({ node }) => <CodeBlock className="col-start-2" {...node.fields} />,
cta: ({ node }) => <CallToActionBlock {...node.fields} />,
},
})
type Props = {
className?: string
content: Record<string, any>
data: SerializedEditorState
enableGutter?: boolean
enableProse?: boolean
}
const RichText: React.FC<Props> = ({
className,
content,
enableGutter = true,
enableProse = true,
}) => {
if (!content) {
return null
}
} & React.HTMLAttributes<HTMLDivElement>
export default function RichText(props: Props) {
const { className, enableProse = true, enableGutter = true, ...rest } = props
return (
<div
<RichTextWithoutBlocks
converters={jsxConverters}
className={cn(
{
'container ': enableGutter,
'max-w-none': !enableGutter,
'mx-auto prose dark:prose-invert ': enableProse,
'mx-auto prose md:prose-md dark:prose-invert ': enableProse,
},
className,
)}
>
{content &&
!Array.isArray(content) &&
typeof content === 'object' &&
'root' in content &&
serializeLexical({ nodes: content?.root?.children })}
</div>
{...rest}
/>
)
}
export default RichText

View File

@@ -1,128 +0,0 @@
// @ts-nocheck
//This copy-and-pasted from lexical here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts
import type { ElementFormatType, TextFormatType } from '@payloadcms/richtext-lexical/lexical'
import type {
TextDetailType,
TextModeType,
} from '@payloadcms/richtext-lexical/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 DOM_ELEMENT_TYPE = 1
export const DOM_TEXT_TYPE = 3
// Reconciling
export const NO_DIRTY_NODES = 0
export const HAS_DIRTY_NODES = 1
export const FULL_RECONCILE = 2
// Text node modes
export const IS_NORMAL = 0
export const IS_TOKEN = 1
export const IS_SEGMENTED = 2
// IS_INERT = 3
// Text node formatting
export const IS_BOLD = 1
export const IS_ITALIC = 1 << 1
export const IS_STRIKETHROUGH = 1 << 2
export const IS_UNDERLINE = 1 << 3
export const IS_CODE = 1 << 4
export const IS_SUBSCRIPT = 1 << 5
export const IS_SUPERSCRIPT = 1 << 6
export const IS_HIGHLIGHT = 1 << 7
export const IS_ALL_FORMATTING =
IS_BOLD |
IS_ITALIC |
IS_STRIKETHROUGH |
IS_UNDERLINE |
IS_CODE |
IS_SUBSCRIPT |
IS_SUPERSCRIPT |
IS_HIGHLIGHT
// Text node details
export const IS_DIRECTIONLESS = 1
export const IS_UNMERGEABLE = 1 << 1
// Element node formatting
export const IS_ALIGN_LEFT = 1
export const IS_ALIGN_CENTER = 2
export const IS_ALIGN_RIGHT = 3
export const IS_ALIGN_JUSTIFY = 4
export const IS_ALIGN_START = 5
export const IS_ALIGN_END = 6
// Reconciliation
export const NON_BREAKING_SPACE = '\u00A0'
const ZERO_WIDTH_SPACE = '\u200b'
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'
export const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']')
export const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']')
export const TEXT_TYPE_TO_FORMAT: Record<TextFormatType | string, number> = {
bold: IS_BOLD,
code: IS_CODE,
highlight: IS_HIGHLIGHT,
italic: IS_ITALIC,
strikethrough: IS_STRIKETHROUGH,
subscript: IS_SUBSCRIPT,
superscript: IS_SUPERSCRIPT,
underline: IS_UNDERLINE,
}
export const DETAIL_TYPE_TO_DETAIL: Record<TextDetailType | string, number> = {
directionless: IS_DIRECTIONLESS,
unmergeable: IS_UNMERGEABLE,
}
export const ELEMENT_TYPE_TO_FORMAT: Record<Exclude<ElementFormatType, ''>, number> = {
center: IS_ALIGN_CENTER,
end: IS_ALIGN_END,
justify: IS_ALIGN_JUSTIFY,
left: IS_ALIGN_LEFT,
right: IS_ALIGN_RIGHT,
start: IS_ALIGN_START,
}
export const ELEMENT_FORMAT_TO_TYPE: Record<number, ElementFormatType> = {
[IS_ALIGN_CENTER]: 'center',
[IS_ALIGN_END]: 'end',
[IS_ALIGN_JUSTIFY]: 'justify',
[IS_ALIGN_LEFT]: 'left',
[IS_ALIGN_RIGHT]: 'right',
[IS_ALIGN_START]: 'start',
}
export const TEXT_MODE_TO_TYPE: Record<TextModeType, 0 | 1 | 2> = {
normal: IS_NORMAL,
segmented: IS_SEGMENTED,
token: IS_TOKEN,
}
export const TEXT_TYPE_TO_MODE: Record<number, TextModeType> = {
[IS_NORMAL]: 'normal',
[IS_SEGMENTED]: 'segmented',
[IS_TOKEN]: 'token',
}

View File

@@ -1,209 +0,0 @@
import { BannerBlock } from '@/blocks/Banner/Component'
import { CallToActionBlock } from '@/blocks/CallToAction/Component'
import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component'
import { MediaBlock } from '@/blocks/MediaBlock/Component'
import React, { Fragment, JSX } from 'react'
import { CMSLink } from '@/components/Link'
import { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical'
import type { BannerBlock as BannerBlockProps } from '@/payload-types'
import {
IS_BOLD,
IS_CODE,
IS_ITALIC,
IS_STRIKETHROUGH,
IS_SUBSCRIPT,
IS_SUPERSCRIPT,
IS_UNDERLINE,
} from './nodeFormat'
import type {
CallToActionBlock as CTABlockProps,
MediaBlock as MediaBlockProps,
} from '@/payload-types'
export type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<CTABlockProps | MediaBlockProps | BannerBlockProps | CodeBlockProps>
type Props = {
nodes: NodeTypes[]
}
export function serializeLexical({ nodes }: Props): JSX.Element {
return (
<Fragment>
{nodes?.map((node, index): JSX.Element | null => {
if (node == null) {
return null
}
if (node.type === 'text') {
let text = <React.Fragment key={index}>{node.text}</React.Fragment>
if (node.format & IS_BOLD) {
text = <strong key={index}>{text}</strong>
}
if (node.format & IS_ITALIC) {
text = <em key={index}>{text}</em>
}
if (node.format & IS_STRIKETHROUGH) {
text = (
<span key={index} style={{ textDecoration: 'line-through' }}>
{text}
</span>
)
}
if (node.format & IS_UNDERLINE) {
text = (
<span key={index} style={{ textDecoration: 'underline' }}>
{text}
</span>
)
}
if (node.format & IS_CODE) {
text = <code key={index}>{node.text}</code>
}
if (node.format & IS_SUBSCRIPT) {
text = <sub key={index}>{text}</sub>
}
if (node.format & IS_SUPERSCRIPT) {
text = <sup key={index}>{text}</sup>
}
return text
}
// NOTE: Hacky fix for
// https://github.com/facebook/lexical/blob/d10c4e6e55261b2fdd7d1845aed46151d0f06a8c/packages/lexical-list/src/LexicalListItemNode.ts#L133
// which does not return checked: false (only true - i.e. there is no prop for false)
const serializedChildrenFn = (node: NodeTypes): JSX.Element | null => {
if (node.children == null) {
return null
} else {
if (node?.type === 'list' && node?.listType === 'check') {
for (const item of node.children) {
if ('checked' in item) {
if (!item?.checked) {
item.checked = false
}
}
}
}
return serializeLexical({ nodes: node.children as NodeTypes[] })
}
}
const serializedChildren = 'children' in node ? serializedChildrenFn(node) : ''
if (node.type === 'block') {
const block = node.fields
const blockType = block?.blockType
if (!block || !blockType) {
return null
}
switch (blockType) {
case 'cta':
return <CallToActionBlock key={index} {...block} />
case 'mediaBlock':
return (
<MediaBlock
className="col-start-1 col-span-3"
imgClassName="m-0"
key={index}
{...block}
captionClassName="mx-auto max-w-[48rem]"
enableGutter={false}
disableInnerContainer={true}
/>
)
case 'banner':
return <BannerBlock className="col-start-2 mb-4" key={index} {...block} />
case 'code':
return <CodeBlock className="col-start-2" key={index} {...block} />
default:
return null
}
} else {
switch (node.type) {
case 'linebreak': {
return <br className="col-start-2" key={index} />
}
case 'paragraph': {
return (
<p className="col-start-2" key={index}>
{serializedChildren}
</p>
)
}
case 'heading': {
const Tag = node?.tag
return (
<Tag className="col-start-2" key={index}>
{serializedChildren}
</Tag>
)
}
case 'list': {
const Tag = node?.tag
return (
<Tag className="list col-start-2" key={index}>
{serializedChildren}
</Tag>
)
}
case 'listitem': {
if (node?.checked != null) {
return (
<li
aria-checked={node.checked ? 'true' : 'false'}
className={` ${node.checked ? '' : ''}`}
key={index}
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role="checkbox"
tabIndex={-1}
value={node?.value}
>
{serializedChildren}
</li>
)
} else {
return (
<li key={index} value={node?.value}>
{serializedChildren}
</li>
)
}
}
case 'quote': {
return (
<blockquote className="col-start-2" key={index}>
{serializedChildren}
</blockquote>
)
}
case 'link': {
const fields = node.fields
return (
<CMSLink
key={index}
newTab={Boolean(fields?.newTab)}
reference={fields.doc as any}
type={fields.linkType === 'internal' ? 'reference' : 'custom'}
url={fields.url}
>
{serializedChildren}
</CMSLink>
)
}
default:
return null
}
}
})}
</Fragment>
)
}

View File

@@ -37,40 +37,33 @@ export const seed = async ({
// as well as the collections and globals
// this is because while `yarn seed` drops the database
// the custom `/api/seed` endpoint does not
payload.logger.info(`— Clearing media...`)
payload.logger.info(`— Clearing collections and globals...`)
// clear the database
for (const global of globals) {
await payload.updateGlobal({
slug: global,
data: {
navItems: [],
},
})
}
for (const collection of collections) {
await payload.delete({
collection: collection,
where: {
id: {
exists: true,
await Promise.all(
globals.map((global) =>
payload.updateGlobal({
slug: global,
data: {
navItems: [],
},
},
})
}
depth: 0,
context: {
disableRevalidate: true,
},
}),
),
)
const pages = await payload.delete({
collection: 'pages',
where: {},
})
await Promise.all(
collections.map((collection) => payload.db.deleteMany({ collection, req, where: {} })),
)
payload.logger.info(`— Seeding demo author and user...`)
await payload.delete({
collection: 'users',
depth: 0,
where: {
email: {
equals: 'demo-author@payloadcms.com',
@@ -78,18 +71,8 @@ export const seed = async ({
},
})
const demoAuthor = await payload.create({
collection: 'users',
data: {
name: 'Demo Author',
email: 'demo-author@payloadcms.com',
password: 'password',
},
})
let demoAuthorID: number | string = demoAuthor.id
payload.logger.info(`— Seeding media...`)
const [image1Buffer, image2Buffer, image3Buffer, hero1Buffer] = await Promise.all([
fetchFileByURL(
'https://raw.githubusercontent.com/payloadcms/payload/refs/heads/main/templates/website/src/endpoints/seed/image-post1.webp',
@@ -105,69 +88,91 @@ export const seed = async ({
),
])
const image1Doc = await payload.create({
collection: 'media',
data: image1,
file: image1Buffer,
})
const image2Doc = await payload.create({
collection: 'media',
data: image2,
file: image2Buffer,
})
const image3Doc = await payload.create({
collection: 'media',
data: image2,
file: image3Buffer,
})
const imageHomeDoc = await payload.create({
collection: 'media',
data: image2,
file: hero1Buffer,
})
const [
demoAuthor,
image1Doc,
image2Doc,
image3Doc,
imageHomeDoc,
technologyCategory,
newsCategory,
financeCategory,
designCategory,
softwareCategory,
engineeringCategory,
] = await Promise.all([
payload.create({
collection: 'users',
data: {
name: 'Demo Author',
email: 'demo-author@payloadcms.com',
password: 'password',
},
}),
payload.create({
collection: 'media',
data: image1,
file: image1Buffer,
}),
payload.create({
collection: 'media',
data: image2,
file: image2Buffer,
}),
payload.create({
collection: 'media',
data: image2,
file: image3Buffer,
}),
payload.create({
collection: 'media',
data: image2,
file: hero1Buffer,
}),
payload.logger.info(`— Seeding categories...`)
const technologyCategory = await payload.create({
collection: 'categories',
data: {
title: 'Technology',
},
})
payload.create({
collection: 'categories',
data: {
title: 'Technology',
},
}),
const newsCategory = await payload.create({
collection: 'categories',
data: {
title: 'News',
},
})
payload.create({
collection: 'categories',
data: {
title: 'News',
},
}),
const financeCategory = await payload.create({
collection: 'categories',
data: {
title: 'Finance',
},
})
payload.create({
collection: 'categories',
data: {
title: 'Finance',
},
}),
payload.create({
collection: 'categories',
data: {
title: 'Design',
},
}),
await payload.create({
collection: 'categories',
data: {
title: 'Design',
},
})
payload.create({
collection: 'categories',
data: {
title: 'Software',
},
}),
await payload.create({
collection: 'categories',
data: {
title: 'Software',
},
})
payload.create({
collection: 'categories',
data: {
title: 'Engineering',
},
}),
])
await payload.create({
collection: 'categories',
data: {
title: 'Engineering',
},
})
let demoAuthorID: number | string = demoAuthor.id
let image1ID: number | string = image1Doc.id
let image2ID: number | string = image2Doc.id
@@ -188,6 +193,10 @@ export const seed = async ({
// This way we can sort them by `createdAt` or `publishedAt` and they will be in the expected order
const post1Doc = await payload.create({
collection: 'posts',
depth: 0,
context: {
disableRevalidate: true,
},
data: JSON.parse(
JSON.stringify({ ...post1, categories: [technologyCategory.id] })
.replace(/"\{\{IMAGE_1\}\}"/g, String(image1ID))
@@ -198,6 +207,10 @@ export const seed = async ({
const post2Doc = await payload.create({
collection: 'posts',
depth: 0,
context: {
disableRevalidate: true,
},
data: JSON.parse(
JSON.stringify({ ...post2, categories: [newsCategory.id] })
.replace(/"\{\{IMAGE_1\}\}"/g, String(image2ID))
@@ -208,6 +221,10 @@ export const seed = async ({
const post3Doc = await payload.create({
collection: 'posts',
depth: 0,
context: {
disableRevalidate: true,
},
data: JSON.parse(
JSON.stringify({ ...post3, categories: [financeCategory.id] })
.replace(/"\{\{IMAGE_1\}\}"/g, String(image3ID))
@@ -217,43 +234,35 @@ export const seed = async ({
})
// update each post with related posts
await payload.update({
id: post1Doc.id,
collection: 'posts',
data: {
relatedPosts: [post2Doc.id, post3Doc.id],
},
})
await payload.update({
id: post2Doc.id,
collection: 'posts',
data: {
relatedPosts: [post1Doc.id, post3Doc.id],
},
})
await payload.update({
id: post3Doc.id,
collection: 'posts',
data: {
relatedPosts: [post1Doc.id, post2Doc.id],
},
})
payload.logger.info(`— Seeding home page...`)
await payload.create({
collection: 'pages',
data: JSON.parse(
JSON.stringify(home)
.replace(/"\{\{IMAGE_1\}\}"/g, String(imageHomeID))
.replace(/"\{\{IMAGE_2\}\}"/g, String(image2ID)),
),
})
await Promise.all([
payload.update({
id: post1Doc.id,
collection: 'posts',
data: {
relatedPosts: [post2Doc.id, post3Doc.id],
},
}),
payload.update({
id: post2Doc.id,
collection: 'posts',
data: {
relatedPosts: [post1Doc.id, post3Doc.id],
},
}),
payload.update({
id: post3Doc.id,
collection: 'posts',
data: {
relatedPosts: [post1Doc.id, post2Doc.id],
},
}),
])
payload.logger.info(`— Seeding contact form...`)
const contactForm = await payload.create({
collection: 'forms',
depth: 0,
data: JSON.parse(JSON.stringify(contactFormData)),
})
@@ -263,74 +272,88 @@ export const seed = async ({
contactFormID = `"${contactFormID}"`
}
payload.logger.info(`— Seeding contact page...`)
payload.logger.info(`— Seeding pages...`)
const contactPage = await payload.create({
collection: 'pages',
data: JSON.parse(
JSON.stringify(contactPageData).replace(/"\{\{CONTACT_FORM_ID\}\}"/g, String(contactFormID)),
),
})
const [_, contactPage] = await Promise.all([
payload.create({
collection: 'pages',
depth: 0,
data: JSON.parse(
JSON.stringify(home)
.replace(/"\{\{IMAGE_1\}\}"/g, String(imageHomeID))
.replace(/"\{\{IMAGE_2\}\}"/g, String(image2ID)),
),
}),
payload.create({
collection: 'pages',
depth: 0,
data: JSON.parse(
JSON.stringify(contactPageData).replace(
/"\{\{CONTACT_FORM_ID\}\}"/g,
String(contactFormID),
),
),
}),
])
payload.logger.info(`— Seeding header...`)
payload.logger.info(`— Seeding globals...`)
await payload.updateGlobal({
slug: 'header',
data: {
navItems: [
{
link: {
type: 'custom',
label: 'Posts',
url: '/posts',
},
},
{
link: {
type: 'reference',
label: 'Contact',
reference: {
relationTo: 'pages',
value: contactPage.id,
await Promise.all([
payload.updateGlobal({
slug: 'header',
data: {
navItems: [
{
link: {
type: 'custom',
label: 'Posts',
url: '/posts',
},
},
},
],
},
})
payload.logger.info(`— Seeding footer...`)
await payload.updateGlobal({
slug: 'footer',
data: {
navItems: [
{
link: {
type: 'custom',
label: 'Admin',
url: '/admin',
{
link: {
type: 'reference',
label: 'Contact',
reference: {
relationTo: 'pages',
value: contactPage.id,
},
},
},
},
{
link: {
type: 'custom',
label: 'Source Code',
newTab: true,
url: 'https://github.com/payloadcms/payload/tree/main/templates/website',
],
},
}),
payload.updateGlobal({
slug: 'footer',
data: {
navItems: [
{
link: {
type: 'custom',
label: 'Admin',
url: '/admin',
},
},
},
{
link: {
type: 'custom',
label: 'Payload',
newTab: true,
url: 'https://payloadcms.com/',
{
link: {
type: 'custom',
label: 'Source Code',
newTab: true,
url: 'https://github.com/payloadcms/payload/tree/main/templates/website',
},
},
},
],
},
})
{
link: {
type: 'custom',
label: 'Payload',
newTab: true,
url: 'https://payloadcms.com/',
},
},
],
},
}),
])
payload.logger.info('Seeded database successfully!')
}

View File

@@ -1,20 +0,0 @@
import { type PayloadHandler } from 'payload'
import { seed as seedScript } from '@/endpoints/seed'
export const seedHandler: PayloadHandler = async (req): Promise<Response> => {
const { payload, user } = req
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
await seedScript({ payload, req })
return Response.json({ success: true })
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error'
payload.logger.error(message)
return Response.json({ error: message }, { status: 500 })
}
}

View File

@@ -19,6 +19,9 @@ export const linkGroup: LinkGroupType = ({ appearances, overrides = {} } = {}) =
appearances,
}),
],
admin: {
initCollapsed: true,
},
}
return deepMerge(generatedLinkGroup, overrides)

View File

@@ -21,10 +21,10 @@ export const HighImpactHero: React.FC<Page['hero']> = ({ links, media, richText
data-theme="dark"
>
<div className="container mb-8 z-10 relative flex items-center justify-center">
<div className="max-w-[36.5rem] text-center">
{richText && <RichText className="mb-6" content={richText} enableGutter={false} />}
<div className="max-w-[36.5rem] md:text-center">
{richText && <RichText className="mb-6" data={richText} enableGutter={false} />}
{Array.isArray(links) && links.length > 0 && (
<ul className="flex justify-center gap-4">
<ul className="flex md:justify-center gap-4">
{links.map(({ link }, i) => {
return (
<li key={i}>

View File

@@ -18,7 +18,7 @@ export const LowImpactHero: React.FC<LowImpactHeroType> = ({ children, richText
return (
<div className="container mt-16">
<div className="max-w-[48rem]">
{children || (richText && <RichText content={richText} enableGutter={false} />)}
{children || (richText && <RichText data={richText} enableGutter={false} />)}
</div>
</div>
)

View File

@@ -10,7 +10,7 @@ export const MediumImpactHero: React.FC<Page['hero']> = ({ links, media, richTex
return (
<div className="">
<div className="container mb-8">
{richText && <RichText className="mb-6" content={richText} enableGutter={false} />}
{richText && <RichText className="mb-6" data={richText} enableGutter={false} />}
{Array.isArray(links) && links.length > 0 && (
<ul className="flex gap-4">
@@ -36,7 +36,7 @@ export const MediumImpactHero: React.FC<Page['hero']> = ({ links, media, richTex
/>
{media?.caption && (
<div className="mt-3">
<RichText content={media.caption} enableGutter={false} />
<RichText data={media.caption} enableGutter={false} />
</div>
)}
</div>

View File

@@ -106,15 +106,41 @@ export default {
},
typography: ({ theme }) => ({
DEFAULT: {
css: {
'--tw-prose-body': 'var(--text)',
'--tw-prose-headings': 'var(--text)',
h1: {
fontSize: '3.5rem',
fontWeight: 'normal',
marginBottom: '0.25em',
css: [
{
'--tw-prose-body': 'var(--text)',
'--tw-prose-headings': 'var(--text)',
h1: {
fontWeight: 'normal',
marginBottom: '0.25em',
},
},
},
],
},
base: {
css: [
{
h1: {
fontSize: '2.5rem',
},
h2: {
fontSize: '1.25rem',
fontWeight: 600,
},
},
],
},
md: {
css: [
{
h1: {
fontSize: '3.5rem',
},
h2: {
fontSize: '1.5rem',
},
},
],
},
}),
},

View File

@@ -1,5 +1,5 @@
{
"id": "7b4ead3c-1d17-460a-b8a8-7b202a4fa16c",
"id": "99a9cc07-6ed3-4eeb-bc7d-a20cc21ec135",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",

View File

@@ -1,9 +1,9 @@
import * as migration_20241204_154138_initial from './20241204_154138_initial'
import * as migration_20241205_211431_initial from './20241205_211431_initial'
export const migrations = [
{
up: migration_20241204_154138_initial.up,
down: migration_20241204_154138_initial.down,
name: '20241204_154138_initial',
up: migration_20241205_211431_initial.up,
down: migration_20241205_211431_initial.down,
name: '20241205_211431_initial',
},
]

View File

@@ -1,5 +1,5 @@
{
"id": "965f2138-755a-4bb6-b83c-ad7827a6a425",
"id": "caad9ebf-d475-40cd-930e-059daf04fde8",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",

View File

@@ -1,9 +1,9 @@
import * as migration_20241204_154113_initial from './20241204_154113_initial'
import * as migration_20241205_211405_initial from './20241205_211405_initial'
export const migrations = [
{
up: migration_20241204_154113_initial.up,
down: migration_20241204_154113_initial.down,
name: '20241204_154113_initial',
up: migration_20241205_211405_initial.up,
down: migration_20241205_211405_initial.down,
name: '20241205_211405_initial',
},
]

View File

@@ -1,14 +1,20 @@
/** @type {import('next-sitemap').IConfig} */
const SITE_URL =
process.env.NEXT_PUBLIC_SERVER_URL ||
process.env.VERCEL_PROJECT_PRODUCTION_URL ||
'https://example.com'
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: SITE_URL,
generateRobotsTxt: true,
exclude: ['/posts-sitemap.xml', '/pages-sitemap.xml', '/*', '/posts/*'],
robotsTxtOptions: {
policies: [
{
userAgent: '*',
disallow: '/admin/*',
},
],
additionalSitemaps: [`${SITE_URL}/pages-sitemap.xml`, `${SITE_URL}/posts-sitemap.xml`],
},
}

Some files were not shown because too many files have changed in this diff Show More