Skip to content

feat(plugin-ecommerce): add locale-aware currency formatting and symbol positioning#15136

Closed
teastudiopl wants to merge 67 commits intopayloadcms:mainfrom
teastudiopl:feat/plugin-ecommerce-currency-symbol
Closed

feat(plugin-ecommerce): add locale-aware currency formatting and symbol positioning#15136
teastudiopl wants to merge 67 commits intopayloadcms:mainfrom
teastudiopl:feat/plugin-ecommerce-currency-symbol

Conversation

@teastudiopl
Copy link
Contributor

@teastudiopl teastudiopl commented Jan 8, 2026

This PR introduces automatic, locale-aware currency formatting using the native Intl.NumberFormat API.

It also standardizes currency symbol positions and separators across the frontend, ensuring consistent price display for different currencies in Payload e-commerce.

Changes

  1. useCurrency hook
  • Added locale support in formatCurrency.
  • Replaced manual string formatting with Intl.NumberFormat:
return new Intl.NumberFormat(locale, {
  style: 'currency',
  currency: code,
  minimumFractionDigits: decimals,
  maximumFractionDigits: decimals,
}).format(value / Math.pow(10, decimals))

Automatically handles:

  • Decimal separators (. vs ,) depending on locale
  • Currency placement (before or after the value)
  • Correct number of fraction digits
  • Optional locale parameter allows overriding per call (default: 'en')

2. Currency display settings

Standardized symbol placement and separator:

<div className="priceCell">
  {currency.symbolPosition === 'before' ? `<span className="currencySymbol">${currency.symbol}</span>` : ''}
  {currency.symbolPosition === 'before' && currency.symbolSeparator}
  <span className="priceValue">{convertFromBaseValue({ baseValue: cellData, currency })}</span>
  {currency.symbolPosition === 'after' && currency.symbolSeparator}
  {currency.symbolPosition === 'after' ? `<span className="currencySymbol">${currency.symbol}</span>` : ''}
</div>

Ensures correct rendering for currencies that place the symbol before (EUR, USD, GBP) or after (PLN).

3. Predefined currencies

Updated standard currency definitions:

export const EUR: Currency = { code: 'EUR', decimals: 2, label: 'Euro', symbol: '€', symbolPosition: 'before', symbolSeparator: '' }
export const USD: Currency = { code: 'USD', decimals: 2, label: 'US Dollar', symbol: '$', symbolPosition: 'before', symbolSeparator: '' }
export const GBP: Currency = { code: 'GBP', decimals: 2, label: 'British Pound', symbol: '£', symbolPosition: 'before', symbolSeparator: '' }

4. Example: currenciesConfig in Payload

import { EUR as BaseEUR, USD } from '@payloadcms/plugin-ecommerce';
import type { Currency } from '@payloadcms/plugin-ecommerce/types';

const PLN = {
  code: 'PLN',
  decimals: 2,
  label: 'Polski złoty',
  symbol: 'zł',
  symbolPosition: 'after',
  symbolSeparator: ' ',
} satisfies Currency;

export const currenciesConfig = {
  supportedCurrencies: [
    PLN,
    USD,
    BaseEUR,
  ],
  defaultCurrency: 'PLN',
};
product-example

teastudiopl and others added 30 commits January 7, 2026 22:25
Add support for displaying currency symbols before or after the price value,
with optional separator between symbol and amount.
# ⚠️ Security Issue

A high-severity Denial of Service
([CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184)) and
a medium-severity Source Code Exposure
([CVE-2025-55183](https://www.cve.org/CVERecord?id=CVE-2025-55183))
affect React 19 and frameworks that use it, like Next.js.

## Summary

Denial of Service
([CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184))

> A malicious HTTP request can be crafted and sent to any App Router
endpoint that, when deserialized, can cause the server process to hang
and consume CPU.

Source Code Exposure
([CVE-2025-55183](https://www.cve.org/CVERecord?id=CVE-2025-55183))

> A malicious HTTP request can be crafted and sent to any App Router
endpoint that can return the compiled source code of Server Actions.
This could reveal business logic, but would not expose secrets unless
they were hardcoded directly into the Server Action’s code.

Full details here:
https://vercel.com/kb/bulletin/security-bulletin-cve-2025-55184-and-cve-2025-55183#how-to-upgrade-and-protect-your-next.js-app

While this is **not a Payload vulnerability,** it may affect any Payload
project running on the affected versions of Next.js. Payload does not
install any of these dependencies directly, it simply _enforces_ their
versions through its peer dependencies, which will only _warn_ you of
the version incompatibilities.

You will need to upgrade React and Next.js yourself in your own apps to
the patched versions listed below in order to receive these updates.

## Resolution

You are strongly encouraged to upgrade your own apps to the nearest
patched versions of Next.js and deploy immediately.

Quick steps:

If using `pnpm` as your package manager, here's a one-liner:

```
pnpm add next@15.4.9
```

For a full breakdown of the vulnerable packages and their patched
releases, see
https://vercel.com/kb/bulletin/security-bulletin-cve-2025-55184-and-cve-2025-55183#how-to-upgrade-and-protect-your-next.js-app.

Related: payloadcms#14807
…ks (payloadcms#14856)

### What?
Updates the `previousValue` data sent to the `afterChange` hook to
accomodate **lexical nested** paths.

### Why?
Currently the `afterChange` hook uses the previousDoc:
```ts
previousValue: getNestedValue(previousDoc, pathSegments) ?? previousDoc?.[field.name],
```

However, with fields from within Lexical, the data on the full
`previousDoc` has a complex nesting structure. In this case, we should
be using the `previousSiblingDoc` to only return the sibling data and
easily de-structure the data.

### How?
Checks if sibling data exists, and uses it if it does:
```ts
 const previousValData =
    previousSiblingDoc && Object.keys(previousSiblingDoc).length > 0
      ? previousSiblingDoc
      : previousDoc
```

**Reported by client.**

Related PR: payloadcms#14582

---------

Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
…onfig page (payloadcms#14905)

The link despite written as "Collection Access Control", currently point
to the general Access Control page and not the dedicated page for
Collection Access Control. On the same paragraph there's also a
reference to the Access Control page and makes it confusing.
This replace the link to point to the dedicated Collection Access
Control page.
…ayloadcms#14903)

### What?
Fix crashing due to unsafe permissions property access in
`AddNewRelation` by safely checking that a collection’s permissions
exist before reading its `create` property.

### Why?
Accessing `permissions.collections[slug].create` throws and breaks the
UI when a collection’s permissions entry is missing.
This happens for example when collection‑level access control makes that
collection inaccessible.

### How?
Change the condition to use optional chaining on the permissions,
aligning with usage elsewhere in the codebase:
- Before: `permissions.collections[relatedCollection?.slug].create`
- After: `permissions.collections[relatedCollection?.slug]?.create`
payloadcms#14906)

Use `getTranslation` for translating localized locale names. Previously,
we were using the current **locale** to get the locale label i18n entry.
This is incorrect - we need to use the current i18n language, which is
what `getTranslation` does.

Fixes payloadcms#14875
…loadcms#14911)

### What?

There is an unused variable in one of the i18n custom translation
examples.

### Why?

Improve overall readability of documentation.

### How?

Remove the unused variable.
Fixes payloadcms#14900. Supersedes
payloadcms#14901.

Since payloadcms#14869, loading the
admin panel with a `serverURL` crashes with the following error:

```
⨯ [TypeError: Invalid URL] {
  code: 'ERR_INVALID_URL',
  input: 'http://localhost:3000http://localhost:3000/admin',
  digest: '185251315'
}
[12:26:08] ERROR: Failed to create URL object from URL: http://localhost:3000http://localhost:3000/admin, falling back to http://localhost:3000http://localhost:3000/admin
```

This is because the suffix used to create a local req object was changed
to include the server URL. As the name implies, it should be a relative
path that appends onto the base URL.
Follow up to payloadcms#14907. Fixes
payloadcms#14900.

Since payloadcms#14869, setting a
`serverURL` causes some additional admin routing to break, including
`/admin/logout`.

We need to ensure that all routes are relative before matching them.

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
Adds a `relative` flag to the `formatAdminURL` util.

There are cases where this function can produce a fully qualified URL,
like for client-side routing, and other times when it must remain
relative, like when matching routes.

This flag differentiates these two behaviors declaratively so we don't
have to rely on the omission of `serverURL`.

```ts
const result = formatAdminURL({
  adminRoute: '/admin',
  basePath: '/v1',
  path: '/collections/posts',
  serverURL: 'http://payloadcms.com',
  relative: true,
})

// returns '/v1/admin/collections/posts'
```

Related: payloadcms#14919 and
payloadcms#14907
…navigation (payloadcms#14910)

Previously, the Popup contents were displayed relative to the Popup
trigger button. This caused the Popup contents to be hidden if the
parent element has `overflow: hidden`, which was the case for
doc_controls on smaller screen sizes (this PR adds an e2e test that
previously failed).

This PR refactors the Popup component to use `createPortal`, rendering
the popup content directly to `document.body` to avoid clipping issues.


## Before


https://github.com/user-attachments/assets/c1cc341a-2295-45b6-89d0-7e89866e5bfd

## After


https://github.com/user-attachments/assets/2eb0d8b5-105e-468b-bd66-8ef8261a6306



While refactoring, I also improved the keyboard navigation for popups:

| Feature | Before | After |
|---------|--------|-------|
| Tab cycling | Exited popup after last button | Cycles and goes back to
first button |
| Escape key | Did nothing | Closes popup and returns focus to trigger |
| Arrow keys | Scrolled the page | Navigates between buttons (↑/↓) |
| Button focus styling | Bad | Good |
| `<a>` elements | Skipped during navigation | Included in keyboard
navigation |


## Keyboard navigation before


https://github.com/user-attachments/assets/9d212c90-7759-42cf-ad37-d7ee7dc64c5d

## Keyboard navigation after


https://github.com/user-attachments/assets/dbc7d647-b58b-4e5b-928c-a6f3fdab6348
## Summary

Adds HTTP Range request support (RFC 7233) to Payload's core file
serving, enabling video streaming with scrubbing/seeking capabilities in
browsers.

## Changes

- **Added `Accept-Ranges: bytes` header** to all file responses
- **Created `parseRangeHeader` utility** for parsing Range headers using
`range-parser` package
- **Enhanced `streamFile` function** to support byte range streaming via
`fs.createReadStream` options
- **Added integration tests** covering various range request scenarios

## Technical Details

- Uses `range-parser` library for RFC 7233 compliant parsing
  - Supports single byte ranges (e.g., `bytes=0-1023`)
  - Handles open-ended ranges (e.g., `bytes=1024-`)
  - Handles suffix ranges (e.g., `bytes=-512`)
- Multi-range requests return first range only (standard simplification)

## Testing

Added comprehensive integration test suite covering:
- Full file requests with Accept-Ranges header
- Partial content requests (206 responses)
- Invalid range handling (416 responses)
- Response body size verification
- Edge cases (out-of-bounds, malformed headers)


### Before


https://github.com/user-attachments/assets/065060ae-35db-4c3d-bc72-cfb976b57349

### After


https://github.com/user-attachments/assets/b70caa49-e055-47b2-87f8-31f02a42c86a
Just adds buttons for logging in with different types of users in the
plugin-multi-tenant test config.

<img width="658" height="863" alt="CleanShot 2025-12-16 at 09 31 49"
src="https://github.com/user-attachments/assets/7bfda8ec-8da7-44b0-892c-4f6fabe327e7"
/>
…here query size (payloadcms#14944)

Before the plugin was looping over relationTo arrays and would build up
duplicate tenant queries. Now it just ensures that there is at lease 1
tenant enabled collection and adds a single tenant constraint to the
filterOptions.

Helpful to note that the relationships are populated 1 at a time and the
filterOptions receives `relationTo` arg, the injected filter checks to
see if the requested relation is a tenant enabled collection also before
applying the constraint.
…new documents saved as drafts when readVersions permissions are false (payloadcms#14950)

### What?

Fixes incorrect `status` display when creating and saving new draft
documents in collections where users don't have readVersions permission.

### Why?

The `getVersions` function was incorrectly assuming any document with an
ID is published, causing the Status component to show "Published" for
newly created drafts. This happens when `readVersions` permission is
disabled because the code takes an early return path that doesn't query
the versions collection.

### How?

- Changed the early return logic in `getVersions.ts` to check the
document's `_status` field directly
- Returns `hasPublishedDoc = false` only when `_status === 'draft'`,
otherwise `true`
Adds dev resources to the base template for a better experience when
building Payload projects with AI tools like Copilot and Cursor.

See more about [AGENTS.md](https://agents.md)

# What's Added

All templates will now ship with an AGENTS.md and .cursor/rules.

1. `AGENTS.md` - https://agents.md
2. `.cursor/rules/` directory
### What

This PR updates the `getGenerateURL` function in the `s3-storage`
adapter to properly encode filenames using `encodeURIComponent` when
constructing file URLs. This ensures that the URL is properly encoded
when using the `disablePayloadAccessControl` option.

 ### Why

Without URL encoding, filenames containing special characters (spaces,
unicode characters, special symbols, etc.) can break URL generation and
lead to:
- 404 errors when accessing files with special characters 
- Malformed URLs that don't properly resolve 
- Security issues with improperly escaped characters

###  How

The fix wraps the filename parameter with `encodeURIComponent()` in the
URL construction, similar to how it is done in the [storage-vercel-blob
package](https://github.com/payloadcms/payload/blob/53d85574d8405f45423871ccf1da3209371ad2da/packages/storage-vercel-blob/src/generateURL.ts#L12).

###  Example:
- Before: `https://s3.endpoint.com/bucket/my file.jpg` (breaks)
- After: `https://s3.endpoint.com/bucket/my%20file.jpg` (works
correctly)
)

Adds some int tests as a follow up to
payloadcms#14438

---------

Co-authored-by: Jens Becker <info@jhb.software>
#### What?
Improves upload security by closing validation gaps for `PDF` and `SVG`
files, preventing malicious files from bypassing MIME type restrictions.

#### Why?

- **PDFs**: Corrupted PDFs with a manipulated content type could bypass
detection when no `fileTypeFromBuffer` was undefined, allowing
unauthorized file types pass.

- **SVGs**: SVGs can contain malicious scripts that execute when
rendered, currently we do not check the SVG for anything potentially
harmful

#### How?
- Added extension validation: If the PDF returns no buffer, we have an
additional check on the `ext` which closes the validation gap
- Added SVG sanitization: New `validateSvg` utility checks for attack
elements including scripts, event handlers, foreign objects, iframes etc
Replaces `URL.parse()` usage which is now deprecated with `new URL()`.
Fixes payloadcms#14978
It's currently not possible to use `getFieldByPath` to find a field
nested within a block.

For example:

```ts
// This return undefined
const fieldInBlock = getFieldByPath({
  path: 'blocks.block2.text2' ,
  fields: [
    {
      type: 'blocks',
      name: 'blocks',
      blocks: [
        {
          slug: 'block1',
          fields: [
            {
              name: 'text1',
              type: 'text',
            },
          ],
        },
        {
          slug: 'block2',
          fields: [
            {
              name: 'text2',
              type: 'text',
            },
          ],
        },
      ],
    }
  ]
})
```

This is because the function is ignoring block slugs and looping over
each block's fields until one is found. Except they are never found
because the path is not trimmed for the next iteration. If the same
field exists across different blocks, I would also imagine the wrong
schema could get matched.
Fixes: payloadcms#14923

Correctly adds `collectionSlug` and `_strategy` to the `user` when using
MCP operations.
This PR brings over most of the test suite improvements I made
[here](https://github.com/payloadcms/enterprise-plugins/pull/249) to our
payload monorepo.

- Replaces mongodb-memory-server with an actual mongo db using the
mongodb-community-server docker image. This unblocks the vitest
migration PR (payloadcms#14337).
Currently, debugging does not work in that PR - this is due to the
global setup script that has to start the mongo memory db.
- Just like postgres, all mongodb databases now support vector search.
mongodb-atlas-local supports it natively, and for
mongodb-community-server, our docker compose script installs `mongot`,
which unlocks support for vector search. This means we could add [vector
storage/search tests similar to the ones we have for
postgres](https://github.com/payloadcms/payload/blob/main/test/database/postgres-vector.int.spec.ts)
- Int tests now run against both mongodb adapters: mongodb
(mongodb-community-server) and mongodb-atlas (mongodb-atlas-local)
- Adds docker scripts for mongodb, mongodb-atlas, and postgres,
documented in README.md. Updates default db adapter URLs to
automatically pick up databases started by those scripts. This makes it
easier for people cloning the repo to get started with consistent
databases matching CI - no complicated manual installation steps
- Simplified db setup handling locally in CI. In CI, everything is
scoped to `.github/actions/start-database/action.yml`. Locally,
everything is scoped to `test/helpers/db`. Each database adapter now
shares the same username, password and db name
- Use consistent db connection string env variables, all ending with
_URL
- Updates the CONTRIBUTING.md with up-to-date information and adds a new
database section. We now recommend everyone to use those docker scripts
[RFC Here](payloadcms#11862).

You can test the feature by running `pnpm dev dashboard` on this branch.

<details>
<summary>Old (obsolete) example</summary>

See the [comment
below](payloadcms#13683 (comment))
explaining the change in approach we took


https://github.com/user-attachments/assets/96157f83-c5d7-4350-9f31-c014daedb2a8

</details>

### New demo


https://github.com/user-attachments/assets/6c08d8d6-c989-4845-b56f-6d3fbd30b1af

## Future Work 

The following improvements are planned but will be added in the future: 

- fields: You'll be able to define `fields` that a widget receives,
which will serve as props in the component. Why might this be useful?
Imagine a chart widget that can have a weekly, daily, or yearly view. Or
a "count" widget that shows how many documents there are in a collection
(the collection could be a field).
- A11y (EDITED): Okay, I finally went the extra mile here, and you can
reorder and resize it with the keyboard. The screen reader works,
although there's room for improvement.
- Dashboard presets: we're planning to add the ability to create and
share dashboard presets, similar to how [query
presets](https://payloadcms.com/docs/query-presets/overview) work today.
For example, you could build dashboards that adjust based on a variable,
such as a "daily, weekly, or monthly" interval. You could also create
dashboards tailored to different focus areas, like "marketing, sales, or
product."
…tenants (payloadcms#14985)

Fixes payloadcms#14823

The issue is essentially the same problem that PR payloadcms#13229 fixed, but for
a different Lexical feature:

| PR payloadcms#13229 | PR payloadcms#14985 (this one) |
| --- | --- |
| Link Feature with internal links | BlocksFeature with relationship
fields inside blocks |
| Relationship to documents exposes other tenants | Relationship inside
blocks exposes other tenants |

## Root Cause

The multi tenant plugin applies tenant filters to relationship fields
using `addFilterOptionsToFields()`, which only runs on collection
fields. However:

- BlocksFeature defines blocks via feature props rather than collection
fields
- These blocks are processed independently through `sanitizeFields()`  
- Relationship fields inside blocks never receive the collection
baseFilter
paulpopus and others added 19 commits January 8, 2026 20:56
…ertFields flag (payloadcms#14949)

This PR adds a new top-level flag `alwaysInsertFields` in the storage
adapter plugin options to ensure the prefix field is always present in
the schema.

Some configurations have prefix dynamically set by environment, but this
can cause schema/db drift and issues where db migrations are needed, as
well as generated types being different between environments.

Now you can add `alwaysInsertFields: true` at the plugin level so that
the prefix field is always present regardless of what you set in
`prefix`, even when the plugin is disabled:
```
s3Storage({
  alwaysInsertFields: true, // prefix field will always exist in schema
  collections: {
    'media': true,
    'media-with-prefix': {
      prefix: process.env.MEDIA_PREFIX, // can be undefined without causing schema drift
    },
  },
  enabled: process.env.USE_S3 === 'true', // works even when disabled
  // ...
})
```

This is particularly useful for:
- Multi-tenant setups where prefix is set dynamically
- Environments where cloud storage is conditionally enabled (e.g., local
dev vs production)
- Ensuring consistent database schema across all environments

**This will be enabled by default and removed as a flag in Payload v4.**
…revent regression (payloadcms#15028)

Fixes invalid Sass import paths in the compiled @payloadcms/ui output
that currently rely on legacy webpack resolution behavior.

Fixes issue payloadcms#15011 

```
./nodemodules/.pnpm/@payloadcms+ui@3.69.0@type_f6dae4b1d169b9370a166162f9bf6e5f/node_modules/@payloadcms/ui/dist/widgets/CollectionCards/index.scss

Error evaluating Node.js code
Error: Can't find stylesheet to import.
  ╷
1 │ @import 'vars';
  │         ^^^^^^
```

Patch file
```
diff --git a/dist/widgets/CollectionCards/index.scss b/dist/widgets/CollectionCards/index.scss
index 0330e9c..c8776a7 100644
--- a/dist/widgets/CollectionCards/index.scss
+++ b/dist/widgets/CollectionCards/index.scss
@@ -1,4 +1,4 @@
-@import '~@payloadcms/ui/scss';
+@import '../../scss/styles';
 
 @layer payload-default {
   .collections {
```

Running a patch file to adjust this import fixes the issue.

---------

Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…exical block component types and styles (payloadcms#14971)

Preview:
https://payloadcms.com/docs/dynamic/rich-text/blocks?branch=docs/lexical-blocks

This PR creates a new documentation page for lexical blocks, with lots
of images and examples showing how to use the blocks feature and
customize it.
Fixes payloadcms#15059

This bumps all dnd-kit dependencies, which fixes peer dependency issues
between `@dnd-kit/core` and `@dnd-kit/modifiers`
🤖 Automated bump of templates for v3.70.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…e safety (payloadcms#14388)

### What?

This PR adds an opt-in `typescript.strictDraftTypes` config flag that
enables strict type safety for draft mode queries. When enabled, `find`
operations with `draft: true` will correctly type required fields as
optional.

```typescript
export default buildConfig({
  typescript: {
    strictDraftTypes: true,  // <-- defaults to false
  },
  // ...
})
```

### Why?

Support for correct types when creating a draft document was added in
payloadcms#14271, however, the types returned when querying a document (e.g. via
Local API `find`) with `draft: true` still return incorrect types—many
required fields are possibly null but typed as non-null.

Making this change without a flag would be a breaking change for
existing user code, as any queries using a dynamic `draft` parameter
would start showing required fields as optional, requiring null checks
throughout their codebase. By introducing this behind a flag, users can
opt-in when ready, and it will become the default behavior in v4.0.

### How?

- Added `strictDraftTypes` config flag to `typescript` config object
(defaults to `false`)
- Added a generic `TDraft extends boolean` parameter to `find()` and
`findLocal()`
- Added conditional return types that check both `TDraft` and
`GeneratedTypes extends { strictDraftTypes: true }`
- Created `QueryDraftDataFromCollection` type for draft query results
where:
  - `id` is required (always present in query results)
- User-defined required fields are optional (validation skipped for
drafts)
  - System fields like `createdAt`, `updatedAt` are optional
- Updated `DraftTransformCollectionWithSelect` to use the new type for
draft queries
- Modified type generation to add `strictDraftTypes: true` to
`GeneratedTypes` when enabled

---------

Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
…dcms#15091)

Adds MCP tools that can `find` and `update` globals.

---------

Co-authored-by: Steven Ceuppens <steven.ceuppens@x3m.industries>
Co-authored-by: Claude <noreply@anthropic.com>
…ed values. (payloadcms#15097)

Fix payloadcms#14780

There are 2 ways a user can add fields to UploadNode within richtext:
```ts
// 1. Media Collection
export const Media: CollectionConfig = {
  slug: 'media',
  fields: [
    { name: 'alt', type: 'text' }  // ← Default alt for the image
  ],
  upload: true,
}

// 2. UploadFeature in Lexical (Per-instance override)
UploadFeature({
  collections: {
    media: {
      fields: [
        { name: 'alt', type: 'text' }  // ← Context-specific alt per usage
      ]
    }
  }
})
```
This PR updates all upload converters (JSX, HTML async/sync, Slate, and
UI components) to intelligently resolve the `alt` attribute with the
following priority:

1. **UploadFeature `fields.alt`** - Context-specific override
2. **Media collection `alt`** - Default from document
3. **Empty string** - Accessibility fallback (instead of using filename)

This prevents users from having to manually override converters to
achieve proper alt text handling.

**Implementation note:** Type assertions are required since both `alt`
fields are user-defined and don't exist in the core types at compile
time.
…#14928)

### Summary

Adds the `skipSync` property which allows you to conditionally skip
syncing documents to the search index based on locale, document
properties, or other criteria.

### Changes

- Added `skipSync` function that returns boolean (true = skip, false =
sync)
- Called once per locale+document combination
- Added documentation with multi-tenant example

### API

```ts
skipSync: async ({ locale, doc, collectionSlug, req }) => {
  if (!locale) return false
  
  const tenant = await req.payload.findByID({
    collection: 'tenants',
    id: doc.tenant.id,
  })
  
  return !tenant.allowedLocales.includes(locale)
}
```

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
…ayloadcms#15102)

This PR deprecates returning `{ state: 'failed' }`, which unnecessarily
inflates the API.

**Reasons:**
- It duplicates existing behavior, making the API and docs more
confusing.
- It creates ambiguity about which approach developers should use.
- It increases maintenance cost.
- Throwing errors is more flexible: for example, cancelling a job from a
task handler currently requires throwing `JobCancelledError`, which
cannot be done via a return value.


# Migration Example

## Before

```ts
import type { TaskConfig } from 'payload'

export const ReturnCustomErrorTask: TaskConfig<'ReturnCustomError'> = {
  retries: 0,
  slug: 'ReturnCustomError',
  inputSchema: [],
  outputSchema: [],
  handler: ({ input }) => {
    return {
      state: 'failed',
      errorMessage: 'oh no! this failed'
    }
  },
}
```

## After

```ts
import type { TaskConfig } from 'payload'

export const ReturnCustomErrorTask: TaskConfig<'ReturnCustomError'> = {
  retries: 0,
  slug: 'ReturnCustomError',
  inputSchema: [],
  outputSchema: [],
  handler: ({ input }) => {
    throw new Error('oh no! this failed')
  },
}
```
…cms#14872)

### What?
Optimizes the count of a join field when the goal is just to retrieve
the count value (i.e. limit: 0).

### Why?
Generates 1 database query instead of two when the goal is just to
retrieve the count value.

### How?
By using payload.count() instead of payload.find()

Co-authored-by: Ricardo Tavares <rtavares@cloudflare.com>
…s#15101)

### What

Crop width/height inputs can now be incremented to the original image
dimensions

### Why

Users were unable to increment crop dimensions back to the full original
size using arrow keys. The inputs would max out at `originalSize - 1`,
even though typing the max value directly worked fine.

### How

Removed redundant pixel-based validation in `fineTuneCrop` that was
blocking state updates when dimensions equaled the original size. The
percentage-based validation already handles invalid values (≤0% and
>100%).

Fixes payloadcms#14758
…ayloadcms#15094)

### What?
Adds recommended serverExternalPackages to the Cloudflare template

### Why?
To make packages with cloudflare-specific code work with OpenNext.
More details here: https://opennext.js.org/cloudflare/howtos/workerd

### How?
By adding the serverExternalPackages property to next.config.ts

May fix payloadcms#14656
Also helps when connecting to Postgres instead of D1 as mentioned in
payloadcms#14181

Co-authored-by: Ricardo Tavares <rtavares@cloudflare.com>
…cms#15098)

### What?
Improves the CLI detection in the Cloudflare template

### Why?
To also support running scripts via `payload run`

### How?
By looking for Payload's bin.js inside process.argv instead of just
looking for the generate/migrate commands.

Discord discussion:
https://discord.com/channels/967097582721572934/1439728398405468291

Co-authored-by: Ricardo Tavares <rtavares@cloudflare.com>
Updates documentation on how to properly fail a task. Fixes
payloadcms#12050

Includes:

- Two approaches to failing tasks (throw vs return failed state)
- Examples with and without custom error messages
- Guidance on when to use each approach
- How to access failure information from job logs
Fixes the block/blockReferences tests that keep failing in CI.
@teastudiopl teastudiopl changed the title feat(plugin-ecommerce) Add locale-aware currency formatting and symbol positioning feat(plugin-ecommerce): Add locale-aware currency formatting and symbol positioning Jan 8, 2026
@teastudiopl teastudiopl changed the title feat(plugin-ecommerce): Add locale-aware currency formatting and symbol positioning feat(plugin-ecommerce): add locale-aware currency formatting and symbol positioning Jan 8, 2026
@paulpopus paulpopus self-assigned this Jan 8, 2026
@paulpopus
Copy link
Contributor

Hey, I appreciate this PR but it seems like something got really messed up in your branch

Best way to fix it would be to copy your changes into new branch that's in sync with our latest main

@teastudiopl teastudiopl closed this Jan 8, 2026
@teastudiopl
Copy link
Contributor Author

new PR: #15139

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.