Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/aurora/news/+core-widgets.documentation
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Documented the new core widgets, the choices widget in the widget lookup order, and the file and image fields in the upgrade guide. @sneridagh
12 changes: 8 additions & 4 deletions docs/conceptual-guides/form-fields-controls-and-widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,14 @@ The form looks the widget up from the field's schema hints, in this order:
1. The field's name, for example `recurrence`.
2. The widget named in the field's tagged values, `frontendOptions.widget`.
3. The field's `widget` hint, for example `textarea` or `datetime`.
4. The field's choices or vocabulary.
5. The field's factory, for example `Relation List`.
6. The field's type, for example `boolean`.
7. The default widget.
4. The field's vocabulary, for example `plone.app.vocabularies.Catalog`, when a widget is registered for it.
5. The choices widget, when the field has choices or a vocabulary.
6. The field's factory, for example `Relation List`.
7. The field's type, for example `boolean`.
8. The default widget.

A widget registered for a vocabulary is more specific than the choices widget, which renders all the other fields with choices or a vocabulary as a select.
Register the choices widget with `config.registerWidget({ key: 'choices', definition: MySelectWidget })`.

Whichever widget it finds, the widget receives the same contract.
That is what lets the form generator stay generic.
Expand Down
93 changes: 91 additions & 2 deletions docs/reference/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ myst:
"description": "The core widgets of Plone Aurora's forms: what they are registered for, the value they read and write, and their widget options"
"property=og:description": "The core widgets of Plone Aurora's forms: what they are registered for, the value they read and write, and their widget options"
"property=og:title": "Core widgets"
"keywords": "Plone Aurora, forms, widgets, widget options, extraFields, object browser, image widget, block schema"
"keywords": "Plone Aurora, forms, widgets, widget options, extraFields, object browser, image widget, select, vocabulary, tags, file upload, block schema"
---

(core-widgets-label)=
Expand Down Expand Up @@ -43,13 +43,20 @@ const blockSchema = {
| --- | --- | --- |
| `TextWidget` | the default widget | a string |
| `TextareaWidget` | `widget: 'textarea'` | a string, which can span several lines |
| `EmailWidget` | `widget: 'email'` | an email address |
| `PasswordWidget` | `widget: 'password'` | a password |
| `UrlWidget` | `widget: 'url'` | an absolute URL |
| `NumberWidget` | `type: 'number'`, `type: 'integer'` | a number, or `null` |
| `BooleanWidget` | `type: 'boolean'` | a boolean |
| `SelectWidget` | the choices widget: fields with `choices` or a `vocabulary`, `widget: 'select'` | the token of the chosen option, or `null` |
| `ArrayWidget` | `type: 'array'`, `widget: 'array'`, `widget: 'token'` | a list of tokens |
| `DateWidget` | `widget: 'date'` | an ISO date, `YYYY-MM-DD`, or `null` |
| `DateTimeWidget` | `widget: 'datetime'` | an ISO 8601 date and time in UTC, such as `2026-10-09T10:00:00Z`, or `null` |
| `AlignWidget` | `widget: 'align'` | the chosen action, such as `left` |
| `SizeWidget` | `widget: 'size'` | the chosen action, such as `m` |
| `WidthWidget` | `widget: 'width'` | the chosen action, such as `full` |
| `ImageWidget` | `widget: 'image'`, `factory: 'Image'` | the image's URL, an app path for site content, or `null` |
| `FileWidget` | `factory: 'File'`, `factory: 'Image'`, `widget: 'file'` | the file, or `null` |
| `ImageWidget` | `widget: 'image'` | the image's URL, an app path for site content, or `null` |
| `ObjectBrowserWidget` | `widget: 'object_browser'`, `factory: 'Relation List'`, `vocabulary: 'plone.app.vocabularies.Catalog'` | a list of the selected items |
| `QuerystringWidget` | `widget: 'querystring'` | a query object |
| `RecurrenceWidget` | the `recurrence` field | an RFC 5545 recurrence rule, or `null` |
Expand All @@ -60,6 +67,63 @@ const blockSchema = {
`TextareaWidget` renders multi-line text, such as the summary of a page.
Neither has widget options.

## Input widgets: `EmailWidget`, `PasswordWidget`, and `UrlWidget`

Text inputs for an email address, a password, and a URL.
On a phone, they show the keyboard for their kind of text.
The browser doesn't fill a password field in with the editor's own password.
None has widget options.

The form checks that the value of a field with `widget: 'email'` or `widget: 'url'` is an email address or a URL, see {ref}`validate-form-fields-label`.

## `NumberWidget`

A number input, with buttons to step the value up and down.
It shows the number in the editor's locale.
The value is a number, or `null` when the editor empties the field.

It reads the schema's `minimum` and `maximum`: the buttons stop there, and a number out of range is brought back within it.
An `integer` field takes whole numbers only.

## `SelectWidget`

A select, where the editor picks one option.
It is the choices widget: it renders the fields with `choices` or a `vocabulary` for which no more specific widget is registered.
The value is the token of the chosen option.

The options are the schema's `choices`, or the terms of the field's vocabulary, which the widget loads from the site.
An optional field also has a "No value" option, which empties the field.

The content API sends some values as a term, an object with its token and its title, such as `{ "token": "en", "title": "English" }`.
The widget reads a term, and writes its token.

## `ArrayWidget`

A list of tokens, such as the tags of a page.
The editor types a token and presses {kbd}`Enter` to add it, and removes a token from the list.
The value is the list of tokens.

The schema's `items` decide which tokens are valid:

- With `choices` or a `vocabulary` in `items`, the editor picks the tokens from those options only.
- Otherwise, the editor can add any token.
A vocabulary in the field's `widgetOptions`, such as the keywords of the `subjects` field, suggests tokens as the editor types.

```ts
days: {
title: 'Days',
type: 'array',
items: {
choices: [
['MO', 'Monday'],
['TU', 'Tuesday'],
],
},
},
```

Like `SelectWidget`, it reads a list of terms, and writes a list of tokens.

## `BooleanWidget`

A checkbox.
Expand Down Expand Up @@ -91,11 +155,36 @@ align: {
},
```

## `FileWidget`

Picks a file for a field that stores a file, such as the file of a File, the image of an Image, or the lead image of a page.
The editor chooses a file, or drops it on the widget.
The widget shows the file's name and size, and a preview of an image.

The value is the file, as the content API sends and receives it.
A new file has its content encoded in base64:

```json
{
"data": "iVBORw0KGgo…",
"encoding": "base64",
"content-type": "image/png",
"filename": "photo.png"
}
```

A stored file comes with its `download` URL instead, and the widget leaves it as it is.
Removing the file sets the value to `null`.

A field with the `Image` factory only takes images.

## `ImageWidget`

Picks an image: from the site with the object browser, by uploading a file, or by entering a URL.
Its value is the image's URL.
For an image of the site, it's an app path, such as `/news/photo.jpg`.
It is for the fields that store the URL of an image, such as the image block's `url`.
A field that stores the image itself uses `FileWidget`.

The widget browses the site from where the form is, and uploads to the form's container.
See {ref}`form-fields-controls-and-widgets-label` for how a widget learns where the form is.
Expand Down
21 changes: 21 additions & 0 deletions docs/upgrade-guide/plone-aurora.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,24 @@ The old names still work, but are deprecated and will be removed in a future rel
```

The `align`, `size`, and `width` widgets that the forms use are now adapters in `@plone/cmsui`, around these pickers.

#### File and image fields store files

```{versionchanged} 1.0.0-alpha.20
The fields with the `File` or `Image` factory use `FileWidget`, instead of `ImageWidget`.
```

```{versionadded} 1.0.0-alpha.20
`SelectWidget`, `ArrayWidget`, `NumberWidget`, `FileWidget`, `EmailWidget`, `PasswordWidget`, and `UrlWidget` in `@plone/cmsui`, and `NumberField` in `@plone/quanta`.
```

A field with the `File` or `Image` factory stores the file itself, such as the image of an Image, or the lead image of a News Item.
`ImageWidget` stored the URL of an image in such a field, which the content API can't save.
These fields now use `FileWidget`, which uploads the file.
`ImageWidget` stays the widget of the fields with `widget: 'image'`, which store the URL of an image, such as the image block's `url`.

If your add-on registered a widget for the `Image` factory, it still overrides `FileWidget`.

Plone Aurora now registers widgets for the fields with choices or a vocabulary, lists, numbers, files, email addresses, passwords, and URLs.
These fields used to render as text inputs.
See {ref}`core-widgets-label`.
1 change: 1 addition & 0 deletions packages/client/news/+content-file-fields.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Accepted the choice tokens of `allow_discussion`, a stored `preview_image`, and `null` for the file fields, in the content data of `createContent` and `updateContent`. @sneridagh
1 change: 1 addition & 0 deletions packages/client/news/+vocabulary-batch-size.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added the `b_size` argument to `getVocabulary`, to get a batch of a given size, or all the terms with `-1`. @sneridagh
7 changes: 6 additions & 1 deletion packages/client/src/restapi/vocabularies/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ const getVocabularySchema = z.object({
title: z.string().optional(),
token: z.string().optional(),
tokens: z.array(z.string()).optional(),
b_size: z.number().optional(),
});

export type VocabulariesArgs = z.infer<typeof getVocabularySchema>;

export async function getVocabulary(
this: PloneClient,
{ path, title, token, tokens }: VocabulariesArgs,
{ path, title, token, tokens, b_size }: VocabulariesArgs,
): Promise<RequestResponse<GetVocabularyResponse>> {
const validatedArgs = getVocabularySchema.parse({
path,
title,
token,
tokens,
b_size,
});

const options: ApiRequestParams = {
Expand All @@ -30,6 +32,9 @@ export async function getVocabulary(
...(validatedArgs.title && { title: validatedArgs.title }),
...(validatedArgs.token && { token: validatedArgs.token }),
...(validatedArgs.tokens && { tokens: validatedArgs.tokens }),
...(validatedArgs.b_size !== undefined && {
b_size: validatedArgs.b_size,
}),
},
};
const vocabulariesPath = `@vocabularies/${validatedArgs.path}`;
Expand Down
18 changes: 14 additions & 4 deletions packages/client/src/validation/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export const createContentDataSchema = z
'@id': z.string().optional(),
'@static_behaviors': z.unknown().optional(),
'@type': z.string(),
allow_discussion: z.boolean().optional(),
// A boolean, or the token of its choices: `True` or `False`.
allow_discussion: z.union([z.boolean(), z.string()]).nullable().optional(),
blocks: z.unknown().optional(),
blocks_layout: z.object({ items: z.array(z.string()) }).optional(),
changeNote: z.string().optional(),
Expand All @@ -82,6 +83,8 @@ export const createContentDataSchema = z
encoding: z.string(),
filename: z.string(),
})
.passthrough()
.nullable()
.optional(),
id: z.string().optional(),
image: z
Expand All @@ -91,6 +94,8 @@ export const createContentDataSchema = z
encoding: z.string(),
filename: z.string(),
})
.passthrough()
.nullable()
.optional(),
language: z.string().optional(),
preview_caption: z.string().optional(),
Expand All @@ -101,6 +106,8 @@ export const createContentDataSchema = z
encoding: z.string(),
filename: z.string(),
})
.passthrough()
.nullable()
.optional(),
relatedItems: z.array(RelatedItemPayloadSchema).optional(),
rights: z.string().nullable().optional(),
Expand All @@ -111,7 +118,8 @@ export const createContentDataSchema = z

export const updateContentDataSchema = z
.object({
allow_discussion: z.boolean().optional(),
// A boolean, or the token of its choices: `True` or `False`.
allow_discussion: z.union([z.boolean(), z.string()]).nullable().optional(),
blocks: z.unknown().optional(),
blocks_layout: z.object({ items: z.array(z.string()) }).optional(),
changeNote: z.string().optional(),
Expand All @@ -130,13 +138,15 @@ export const updateContentDataSchema = z
})
.optional(),
preview_caption: z.string().nullable().optional(),
// A new file, or the stored one as the API sent it.
preview_image: z
.object({
'content-type': z.string(),
data: z.string(),
encoding: z.string(),
data: z.string().optional(),
encoding: z.string().optional(),
filename: z.string(),
})
.passthrough()
.nullable()
.optional(),
relatedItems: z.array(RelatedItemPayloadSchema).optional(),
Expand Down
Loading
Loading