alinea 2.0.0-preview.13 → 2.0.0-preview.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.init/content/pages/welcome.json +1 -1
- package/dist/.mcp/later/dev.json +1 -1
- package/dist/.mcp/online/dev.json +1 -1
- package/dist/adapter/next/cms.js +6 -2
- package/dist/adapter/next/context.js +1 -4
- package/dist/adapter/next/handler.js +12 -1
- package/dist/backend/CommitEntries.d.ts +17 -0
- package/dist/backend/CommitEntries.js +39 -0
- package/dist/backend/Handler.d.ts +9 -0
- package/dist/backend/Handler.js +80 -20
- package/dist/backend/api/BasicAuth.js +5 -1
- package/dist/backend/api/CreateBackend.js +2 -2
- package/dist/backend/api/DatabaseApi.js +18 -5
- package/dist/backend/api/GithubApi.d.ts +13 -0
- package/dist/backend/api/GithubApi.js +170 -30
- package/dist/backend/api/database.js +2 -2
- package/dist/backend/util/Syncable.d.ts +6 -0
- package/dist/backend/util/Syncable.js +3 -1
- package/dist/bundled.d.ts +911 -151
- package/dist/chunks/{chunk-5H4D5JQL.js → chunk-2PP7WJUJ.js} +1 -11
- package/dist/chunks/{chunk-QHHMKJUO.js → chunk-7DDPSWRY.js} +465 -2
- package/dist/chunks/{chunk-HOETF6HN.js → chunk-PMGIIYDD.js} +1 -0
- package/dist/chunks/{chunk-3QFSGTGF.js → chunk-SCV3TEXM.js} +1 -1
- package/dist/chunks/{chunk-UPY2RUZF.js → chunk-TWBREZWC.js} +30 -0
- package/dist/cli/Generate.js +20 -7
- package/dist/cli/Mcp.js +1 -1
- package/dist/cli/Serve.js +1 -1
- package/dist/cli/bin.js +1 -1
- package/dist/cli/build/BuildEmitter.d.ts +5 -1
- package/dist/cli/build/BuildEmitter.js +8 -3
- package/dist/cli/generate/CompileConfig.js +10 -6
- package/dist/cli/generate/DevDB.d.ts +4 -1
- package/dist/cli/generate/DevDB.js +15 -7
- package/dist/cli/generate/GenerateDashboard.js +1 -0
- package/dist/cli/generate/MediaLocations.d.ts +18 -0
- package/dist/cli/generate/MediaLocations.js +69 -0
- package/dist/cli/serve/CreateLocalServer.js +1 -0
- package/dist/cli/serve/mcp/DevMcp.js +1 -1
- package/dist/cloud/CloudRemote.js +21 -7
- package/dist/cms.d.ts +4 -2
- package/dist/components/Alert.js +2 -2
- package/dist/components/AppShell.js +2 -2
- package/dist/components/AsyncList.d.ts +52 -0
- package/dist/components/AsyncList.js +69 -0
- package/dist/components/Breadcrumbs.js +6 -6
- package/dist/components/Button.js +4 -4
- package/dist/components/Calendar.js +18 -18
- package/dist/components/Checkbox.js +10 -10
- package/dist/components/CheckboxGroup.js +2 -2
- package/dist/components/Collapsible.js +5 -5
- package/dist/components/ColorSwatch.js +2 -2
- package/dist/components/ColorSwatchPicker.js +4 -4
- package/dist/components/ComboBox.d.ts +27 -9
- package/dist/components/ComboBox.js +40 -9
- package/dist/components/Command.js +13 -13
- package/dist/components/ContentCard.js +28 -28
- package/dist/components/ContentGrid.js +11 -11
- package/dist/components/DataList.js +1 -1
- package/dist/components/DateField.d.ts +4 -0
- package/dist/components/DateField.js +2 -2
- package/dist/components/DatePicker.js +2 -2
- package/dist/components/DateRangePicker.js +2 -2
- package/dist/components/Dialog.d.ts +14 -3
- package/dist/components/Dialog.js +38 -11
- package/dist/components/DropZone.js +6 -6
- package/dist/components/DropdownMenu.js +8 -8
- package/dist/components/Empty.js +4 -4
- package/dist/components/Field.d.ts +1 -1
- package/dist/components/Field.js +18 -12
- package/dist/components/Form.d.ts +23 -0
- package/dist/components/Form.js +64 -0
- package/dist/components/Link.d.ts +8 -1
- package/dist/components/List.js +27 -27
- package/dist/components/MediaPreview.js +5 -5
- package/dist/components/MultipleSelect.d.ts +13 -8
- package/dist/components/MultipleSelect.js +25 -25
- package/dist/components/NavRail.js +7 -7
- package/dist/components/NumberField.d.ts +5 -1
- package/dist/components/NumberField.js +6 -4
- package/dist/components/Page.js +6 -6
- package/dist/components/Popover.js +2 -2
- package/dist/components/PreviewFrame.js +8 -8
- package/dist/components/RadioGroup.js +10 -10
- package/dist/components/Resizable.js +1 -1
- package/dist/components/SearchField.js +8 -8
- package/dist/components/Select.d.ts +13 -10
- package/dist/components/Select.js +18 -32
- package/dist/components/Sheet.js +6 -6
- package/dist/components/Sidebar.js +5 -5
- package/dist/components/SortableList.js +12 -12
- package/dist/components/Spinner.js +4 -4
- package/dist/components/Stack.d.ts +20 -0
- package/dist/components/Stack.js +45 -0
- package/dist/components/Surface.js +1 -1
- package/dist/components/Switch.js +5 -5
- package/dist/components/Table.d.ts +11 -1
- package/dist/components/Table.js +81 -53
- package/dist/components/Tabs.js +6 -6
- package/dist/components/TagGroup.js +10 -10
- package/dist/components/TextField.d.ts +7 -1
- package/dist/components/TextField.js +20 -11
- package/dist/components/TimeField.d.ts +6 -1
- package/dist/components/TimeField.js +51 -26
- package/dist/components/Timestamp.js +1 -1
- package/dist/components/ToggleGroup.js +2 -2
- package/dist/components/Toolbar.js +3 -3
- package/dist/components/Tooltip.js +5 -4
- package/dist/components/Tree.js +27 -27
- package/dist/components/internal/DateControl.js +11 -11
- package/dist/components/internal/FormScope.d.ts +2 -0
- package/dist/components/internal/FormScope.js +8 -0
- package/dist/components/internal/ListBoxGroup.d.ts +12 -0
- package/dist/components/internal/ListBoxGroup.js +46 -0
- package/dist/components/internal/ListBoxOption.d.ts +2 -2
- package/dist/components/internal/ListBoxOption.js +5 -5
- package/dist/components/internal/Locale.js +2 -2
- package/dist/components/internal/PopoverSurface.js +5 -4
- package/dist/components/internal/SelectionCheckbox.js +5 -5
- package/dist/components/internal/Slot.js +1 -1
- package/dist/components/internal/ToggleButton.js +4 -4
- package/dist/components/internal/Trigger.js +2 -2
- package/dist/components/internal/useDragDrop.js +2 -2
- package/dist/components/types.d.ts +4 -0
- package/dist/components.d.ts +3 -0
- package/dist/components.js +3 -0
- package/dist/core/Client.js +12 -1
- package/dist/core/Config.d.ts +6 -0
- package/dist/core/Config.js +12 -0
- package/dist/core/DashboardEntry.d.ts +19 -0
- package/dist/core/DashboardEntry.js +0 -0
- package/dist/core/Document.d.ts +2 -2
- package/dist/core/Document.js +10 -3
- package/dist/core/EntryFields.d.ts +8 -2
- package/dist/core/Field.d.ts +5 -0
- package/dist/core/Filter.d.ts +19 -2
- package/dist/core/Filter.js +5 -0
- package/dist/core/Graph.d.ts +10 -2
- package/dist/core/Overview.d.ts +74 -5
- package/dist/core/Overview.js +1 -0
- package/dist/core/Root.js +1 -1
- package/dist/core/Scope.d.ts +5 -0
- package/dist/core/Scope.js +22 -5
- package/dist/core/Type.d.ts +25 -6
- package/dist/core/Type.js +25 -7
- package/dist/core/db/LocalStore.d.ts +7 -0
- package/dist/core/field/CreateField.d.ts +1 -2
- package/dist/core/source/Source.d.ts +2 -0
- package/dist/core/source/Source.js +11 -0
- package/dist/core/util/Async.d.ts +2 -0
- package/dist/core/util/Async.js +12 -1
- package/dist/core/util/Checks.js +1 -1
- package/dist/core/util/Slugs.js +4 -3
- package/dist/dashboard/App.css +13 -11
- package/dist/dashboard/App.d.ts +1 -0
- package/dist/dashboard/App.js +22 -6
- package/dist/dashboard/Href.d.ts +37 -0
- package/dist/dashboard/Href.js +46 -0
- package/dist/dashboard/app/ActivityStatus.js +34 -12
- package/dist/dashboard/app/AuthView.js +2 -2
- package/dist/dashboard/app/BlockSettings.d.ts +9 -0
- package/dist/dashboard/app/BlockSettings.js +16 -0
- package/dist/dashboard/app/BlockSheet.js +3 -3
- package/dist/dashboard/app/BlockSheetStoryFrame.js +2 -2
- package/dist/dashboard/app/CompactField.js +15 -15
- package/dist/dashboard/app/CroppedImage.js +2 -2
- package/dist/dashboard/app/DashboardLayout.d.ts +1 -1
- package/dist/dashboard/app/DashboardLayout.js +12 -0
- package/dist/dashboard/app/EntryActions.d.ts +11 -0
- package/dist/dashboard/app/EntryActions.js +30 -0
- package/dist/dashboard/app/EntryFields.d.ts +15 -2
- package/dist/dashboard/app/EntryFields.js +26 -5
- package/dist/dashboard/app/EntryHeader.js +17 -7
- package/dist/dashboard/app/EntryReferences.js +2 -2
- package/dist/dashboard/app/EntrySidebar.js +25 -23
- package/dist/dashboard/app/EntrySidebarPreview.js +2 -2
- package/dist/dashboard/app/EntryValidationModal.js +3 -3
- package/dist/dashboard/app/Explorer.js +84 -45
- package/dist/dashboard/app/ExplorerBatchActions.js +4 -4
- package/dist/dashboard/app/ExplorerControls.js +5 -5
- package/dist/dashboard/app/ExplorerList.js +2 -2
- package/dist/dashboard/app/ExplorerModal.js +3 -3
- package/dist/dashboard/app/ExplorerOverviewHeader.d.ts +6 -0
- package/dist/dashboard/app/ExplorerOverviewHeader.js +28 -0
- package/dist/dashboard/app/ExplorerTable.js +7 -7
- package/dist/dashboard/app/ExplorerTabs.d.ts +12 -0
- package/dist/dashboard/app/ExplorerTabs.js +42 -0
- package/dist/dashboard/app/ImageEditor.js +31 -31
- package/dist/dashboard/app/LinkPicker.js +3 -3
- package/dist/dashboard/app/LocaleMenu.js +9 -9
- package/dist/dashboard/app/MediaPreviewCell.js +5 -5
- package/dist/dashboard/app/OverviewCell.js +12 -10
- package/dist/dashboard/app/RemoveDialog.js +5 -0
- package/dist/dashboard/app/SearchBar.js +4 -4
- package/dist/dashboard/app/SettingsTab.d.ts +6 -0
- package/dist/dashboard/app/SettingsTab.js +27 -0
- package/dist/dashboard/app/SidebarTree.js +19 -19
- package/dist/dashboard/app/WorkspaceMenu.js +6 -6
- package/dist/dashboard/app/WorkspaceRoots.js +102 -98
- package/dist/dashboard/app/editor/FileEditor.js +18 -18
- package/dist/dashboard/app/pages/EntryPage.js +42 -39
- package/dist/dashboard/app/pages/RootPage.js +3 -3
- package/dist/dashboard/app/pages/SplashPage.js +94 -79
- package/dist/dashboard/app/pages/UsersPage.js +9 -9
- package/dist/dashboard/app/ui/DashboardModal.js +14 -14
- package/dist/dashboard/atoms/activity.d.ts +1 -0
- package/dist/dashboard/atoms/activity.js +8 -5
- package/dist/dashboard/atoms/config.d.ts +2 -0
- package/dist/dashboard/atoms/dashboard.d.ts +11 -0
- package/dist/dashboard/atoms/dashboard.js +8 -0
- package/dist/dashboard/atoms/editor.d.ts +7 -1
- package/dist/dashboard/atoms/editor.js +14 -2
- package/dist/dashboard/atoms/explorer.d.ts +15 -0
- package/dist/dashboard/atoms/explorer.js +43 -14
- package/dist/dashboard/atoms/graph.d.ts +5 -0
- package/dist/dashboard/atoms/graph.js +26 -1
- package/dist/dashboard/atoms/nav.d.ts +16 -10
- package/dist/dashboard/atoms/nav.js +96 -36
- package/dist/dashboard/atoms/overview.d.ts +26 -4
- package/dist/dashboard/atoms/overview.js +41 -12
- package/dist/dashboard/atoms/root.js +7 -1
- package/dist/dashboard/boot/ActivityEvent.d.ts +1 -1
- package/dist/dashboard/boot/Boot.js +6 -0
- package/dist/dashboard/boot/DashboardWorker.d.ts +6 -0
- package/dist/dashboard/boot/DashboardWorker.js +89 -8
- package/dist/dashboard/boot/LoadWorker.js +3 -0
- package/dist/dashboard/boot/SyncEvent.d.ts +7 -0
- package/dist/dashboard/boot/SyncEvent.js +14 -0
- package/dist/dashboard/boot/WorkerDB.d.ts +1 -0
- package/dist/dashboard/boot/WorkerDB.js +3 -0
- package/dist/dashboard/cms/EntryTable.d.ts +4 -3
- package/dist/dashboard/cms/EntryTable.js +2 -2
- package/dist/dashboard/cms/ViewProps.d.ts +3 -2
- package/dist/dashboard/cms/hooks.d.ts +34 -18
- package/dist/dashboard/cms/hooks.js +43 -18
- package/dist/dashboard/contexts.d.ts +3 -2
- package/dist/dashboard/contexts.js +3 -2
- package/dist/dashboard/hook/UseEntryEditor.d.ts +1 -1
- package/dist/dashboard/hook/UseEntryEditor.js +7 -3
- package/dist/dashboard/icons.d.ts +1 -0
- package/dist/dashboard/icons.js +15 -0
- package/dist/database/BrowserEntryStore.js +35 -13
- package/dist/database/DatabaseTables.js +1 -1
- package/dist/database/EntryDatabase.d.ts +10 -1
- package/dist/database/EntryDatabase.js +31 -10
- package/dist/database/EntryStore.js +1 -11
- package/dist/database/entry/EntryTable.d.ts +5 -3
- package/dist/database/entry/EntryTable.js +25 -8
- package/dist/database/query/Condition.d.ts +4 -1
- package/dist/database/query/Condition.js +16 -5
- package/dist/database/query/EntryQuery.js +59 -7
- package/dist/database/query/Relation.d.ts +13 -1
- package/dist/database/query/Relation.js +19 -1
- package/dist/database/query/Search.d.ts +7 -1
- package/dist/database/query/Search.js +54 -9
- package/dist/database/sync/EntrySyncer.d.ts +2 -0
- package/dist/database/sync/EntrySyncer.js +4 -2
- package/dist/database/sync/Ingest.d.ts +2 -1
- package/dist/database/sync/Ingest.js +20 -6
- package/dist/field/code/CodeField.view.js +12 -12
- package/dist/field/link/EntryLink.d.ts +4 -5
- package/dist/field/link/FileLink.d.ts +2 -2
- package/dist/field/link/ImageLink.d.ts +2 -5
- package/dist/field/link/ImageLink.js +0 -1
- package/dist/field/link/LinkField.view.js +18 -15
- package/dist/field/list/ListField.view.js +50 -35
- package/dist/field/metadata/MetadataField.view.js +14 -14
- package/dist/field/richtext/RichTextBlock.js +39 -35
- package/dist/field/richtext/RichTextExtensions.js +23 -23
- package/dist/field/richtext/RichTextField.view.js +6 -6
- package/dist/field/richtext/RichTextInsertMenu.js +5 -5
- package/dist/field/richtext/RichTextToolbar.js +26 -1
- package/dist/field/richtext/Toolbar.d.ts +9 -1
- package/dist/field/richtext/Toolbar.js +12 -2
- package/dist/field/select/SelectField.view.js +1 -0
- package/dist/index.css +174 -23
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/next.d.ts +2 -0
- package/dist/next.edge.d.ts +2 -0
- package/dist/picker/entry/EntryPicker.d.ts +3 -1
- package/dist/picker/entry/EntryPicker.js +15 -1
- package/dist/preview/react.js +3 -0
- package/dist/query.d.ts +26 -14
- package/dist/query.js +4 -0
- package/dist/types.d.ts +8 -5
- package/dist/ui/Stack.d.ts +4 -2
- package/docs/ai-setup.md +3 -3
- package/docs/cli.md +4 -2
- package/docs/components/combo-box.md +105 -16
- package/docs/components/date-field.md +1 -1
- package/docs/components/dialog.md +51 -0
- package/docs/components/form.md +67 -0
- package/docs/components/link.md +10 -0
- package/docs/components/multiple-select.md +9 -17
- package/docs/components/number-field.md +5 -1
- package/docs/components/select.md +9 -20
- package/docs/components/stack.md +42 -0
- package/docs/components/table.md +83 -0
- package/docs/components/text-field.md +9 -1
- package/docs/components/time-field.md +5 -1
- package/docs/components.md +39 -1
- package/docs/configuration.md +3 -1
- package/docs/custom-fields.md +135 -7
- package/docs/deploy/self-host.md +2 -0
- package/docs/deploy.md +7 -4
- package/docs/fields/image.md +0 -1
- package/docs/fields/list.md +19 -0
- package/docs/fields/object.md +5 -3
- package/docs/fields/rich-text.md +31 -0
- package/docs/index.md +8 -6
- package/docs/instant-publishing.md +23 -2
- package/docs/overviews.md +128 -4
- package/docs/query/filtering.md +2 -0
- package/docs/query/related.md +58 -9
- package/docs/query.md +52 -1
- package/docs/quickstart.md +5 -5
- package/docs/schema/document.md +2 -0
- package/docs/schema/type.md +3 -1
- package/docs/upgrading.md +100 -19
- package/llms-full.txt +1022 -125
- package/package.json +1 -1
- package/readme.md +2 -2
- package/dist/chunks/{chunk-4ZA6KZHS.js → chunk-6FQ5IMNA.js} +3 -3
package/llms-full.txt
CHANGED
|
@@ -29,22 +29,22 @@ Navigate to the newly created project directory and install the package with you
|
|
|
29
29
|
|
|
30
30
|
Variant: npm
|
|
31
31
|
```shellscript
|
|
32
|
-
npm install alinea@
|
|
32
|
+
npm install alinea@rc
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Variant: yarn
|
|
36
36
|
```shellscript
|
|
37
|
-
yarn add alinea@
|
|
37
|
+
yarn add alinea@rc
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
Variant: pnpm
|
|
41
41
|
```shellscript
|
|
42
|
-
pnpm add alinea@
|
|
42
|
+
pnpm add alinea@rc
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Variant: bun
|
|
46
46
|
```shellscript
|
|
47
|
-
bun add alinea@
|
|
47
|
+
bun add alinea@rc
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## 3. Initialize the project
|
|
@@ -53,7 +53,7 @@ Alinea requires a [config file](https://v2.alineacms.com/docs/configuration) whi
|
|
|
53
53
|
|
|
54
54
|
Variant: npm
|
|
55
55
|
```shellscript
|
|
56
|
-
npx alinea@
|
|
56
|
+
npx alinea@rc init
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Variant: yarn
|
|
@@ -231,15 +231,15 @@ The rest of this page is written for the agent. You are adding Alinea, a git-bas
|
|
|
231
231
|
Install the package in the project root.
|
|
232
232
|
|
|
233
233
|
```shellscript
|
|
234
|
-
npm install alinea@
|
|
234
|
+
npm install alinea@rc
|
|
235
235
|
```
|
|
236
236
|
|
|
237
|
-
With other package managers: `pnpm add alinea@
|
|
237
|
+
With other package managers: `pnpm add alinea@rc`, `yarn add alinea@rc` or `bun add alinea@rc`.
|
|
238
238
|
|
|
239
239
|
## 3. Run alinea init
|
|
240
240
|
|
|
241
241
|
```shellscript
|
|
242
|
-
npx alinea@
|
|
242
|
+
npx alinea@rc init
|
|
243
243
|
```
|
|
244
244
|
|
|
245
245
|
In a Next.js project `alinea init` creates:
|
|
@@ -1948,23 +1948,25 @@ This guide walks through moving an existing Alinea 1.7 project to 2.0. Your cont
|
|
|
1948
1948
|
|
|
1949
1949
|
## Checklist
|
|
1950
1950
|
|
|
1951
|
-
1. Run Node.js 24 or higher, locally, in CI and wherever your site runs, and use React 19.
|
|
1952
|
-
2. Update the package: `npm install alinea@
|
|
1953
|
-
3. Add `handlerUrl` to your config, it is now required. Replace `dashboardFile` with `adminPath`.
|
|
1951
|
+
1. Run Node.js 24 or higher, locally, in CI (scheduled jobs that run the alinea CLI too) and wherever your site runs, and use React 19.
|
|
1952
|
+
2. Update the package: `npm install alinea@rc` (or `pnpm add`, `yarn add`, `bun add`).
|
|
1953
|
+
3. Add `handlerUrl` to your config, it is now required, and set `baseUrl.production` (or make `handlerUrl` absolute). Replace `dashboardFile` with `adminPath`.
|
|
1954
1954
|
4. Make sure your Next.js config is wrapped in `withAlinea` and your scripts run `alinea dev -- next dev` and `alinea build -- next build`: media URLs are now served through your site.
|
|
1955
|
-
5.
|
|
1956
|
-
6.
|
|
1957
|
-
7.
|
|
1958
|
-
8.
|
|
1959
|
-
9.
|
|
1960
|
-
10.
|
|
1961
|
-
11.
|
|
1955
|
+
5. Media: replace `image.url` with `image.src` and use the URLs Alinea returns instead of building them from `location`. `alinea build` warns about media files that store a url as their location, `alinea build --fix` repairs them.
|
|
1956
|
+
6. Move children of `Config.media(...)` into its `children` option.
|
|
1957
|
+
7. Remove the options of `Field.metadata(...)` and the `step` option of `Field.time`.
|
|
1958
|
+
8. Rich text: pass `extensions` as a function, upgrade custom extensions to Tiptap 3 and render custom attributes as `data-*` attributes.
|
|
1959
|
+
9. Handler: replace `remote` with `backend`, and move entry hooks to `beforeCommit` / `afterCommit`.
|
|
1960
|
+
10. Self-hosted OAuth2: provide `validateClaims`.
|
|
1961
|
+
11. Custom field views and dashboard code: import hooks from `alinea/cms` and components from `alinea/components` instead of `alinea/dashboard` and `alinea/ui`, replace `useEntryEditor` with `useEntry` and rename the theme tokens your CSS reads.
|
|
1962
|
+
12. Run `alinea build`, open the dashboard and save an entry to review the diff.
|
|
1963
|
+
13. Upgrading from a 2.0 preview instead? See [From a 2.0 preview](#from-a-2-0-preview).
|
|
1962
1964
|
|
|
1963
1965
|
## Runtime requirements
|
|
1964
1966
|
|
|
1965
|
-
Node.js 24 or higher. The CLI exits with "Alinea requires Node version 24 or higher" on older versions. The bundled content database is opened with Node's built-in `node:sqlite` (or `bun:sqlite` under Bun), so the runtime that serves your site needs Node 24 too, not only the machine that builds it.
|
|
1967
|
+
Node.js 24 or higher. The CLI exits with "Alinea requires Node version 24 or higher" on older versions. The bundled content database is opened with Node's built-in `node:sqlite` (or `bun:sqlite` under Bun), so the runtime that serves your site needs Node 24 too, not only the machine that builds it. The same goes for scheduled CI jobs that run the alinea CLI, such as a nightly `alinea build` or a script that imports content: update the Node version in those workflows as well.
|
|
1966
1968
|
|
|
1967
|
-
React 19. `react` and `react-dom` 19 are now peer dependencies and the CLI checks for them. In practice that means Next.js 15 or newer.
|
|
1969
|
+
React 19. `react` and `react-dom` 19 are now peer dependencies and the CLI checks for them. In practice that means Next.js 15 or newer. Check third-party React renderers too: libraries built on React 18 internals, such as older versions of react-three-fiber or react-pixi, break under React 19. Upgrade them to a version that supports React 19.
|
|
1968
1970
|
|
|
1969
1971
|
## Configuration
|
|
1970
1972
|
|
|
@@ -1994,6 +1996,23 @@ export const cms = createCMS({
|
|
|
1994
1996
|
|
|
1995
1997
|
The handler now matches its path exactly, so requests have to use the configured URL (a trailing slash no longer matches).
|
|
1996
1998
|
|
|
1999
|
+
### baseUrl is required for production
|
|
2000
|
+
|
|
2001
|
+
Your site reaches the handler at `handlerUrl` resolved against `baseUrl`, also in production, to check for new content. Without a production `baseUrl` (and with a relative `handlerUrl`), `alinea build` exits and queries throw `Missing baseUrl in Alinea config: set baseUrl.production to the url of your site`. Alinea doesn't fall back to the request origin or to hosting provider variables: set the production URL of your site yourself, for example from an environment variable you define:
|
|
2002
|
+
|
|
2003
|
+
File: cms.ts
|
|
2004
|
+
```tsx
|
|
2005
|
+
export const cms = createCMS({
|
|
2006
|
+
schema,
|
|
2007
|
+
workspaces,
|
|
2008
|
+
baseUrl: {
|
|
2009
|
+
development: 'http://localhost:3000',
|
|
2010
|
+
production: process.env.SITE_URL ?? 'https://example.com'
|
|
2011
|
+
},
|
|
2012
|
+
handlerUrl: '/api/cms'
|
|
2013
|
+
})
|
|
2014
|
+
```
|
|
2015
|
+
|
|
1997
2016
|
### dashboardFile is replaced by adminPath
|
|
1998
2017
|
|
|
1999
2018
|
`adminPath` is the path the dashboard is served on, `/admin` by default. `alinea build` writes the dashboard to `public/admin.html` and `withAlinea` serves it at `/admin`. `dashboardFile` still works when `adminPath` isn't set, but it is deprecated. See [Configuration](https://v2.alineacms.com/docs/configuration) for the new `maxUploadSize` and `tracer` options.
|
|
@@ -2014,6 +2033,28 @@ What to check:
|
|
|
2014
2033
|
- Media requests now reach your handler route. A static export without the handler can't serve them.
|
|
2015
2034
|
- If you defined `images.localPatterns` or `remotePatterns` yourself, check that the admin file route is still allowed. The `uploads.alinea.cloud` remote pattern is no longer added.
|
|
2016
2035
|
- Don't change the `mediaDir` of an existing workspace: stored locations are relative to it. New projects default to `public/media`.
|
|
2036
|
+
- A media file whose `location` is a url (some 1.x saves wrote the upload url there) can't be served. `alinea build` warns about these files; `alinea build --fix` points them at the file of the same name in the `mediaDir` when there is one.
|
|
2037
|
+
|
|
2038
|
+
Image links only have `src`: the `url` property, which held the same value, was removed so that spreading an image into `<img>` or `next/image` (`<Image {...image} />`) doesn't put a `url` attribute on the element. Replace `image.url` with `image.src`. File links keep `url` and `href`.
|
|
2039
|
+
|
|
2040
|
+
Don't derive media URLs from the stored file. Code that reads `location`, or parses the 1.x file names (`name.<id>.ext`) to build a URL or find an id, breaks on `/admin/file/...` URLs. Use the URL Alinea returns instead: `src` for images, `url` (or `href`) for files, and `_url` when you query media files directly:
|
|
2041
|
+
|
|
2042
|
+
```tsx
|
|
2043
|
+
import {cms} from '@/cms'
|
|
2044
|
+
import {Query} from 'alinea'
|
|
2045
|
+
import {MediaFile} from 'alinea/core'
|
|
2046
|
+
|
|
2047
|
+
// 1.7: built the url from the stored location
|
|
2048
|
+
// const src = file.location
|
|
2049
|
+
|
|
2050
|
+
const files = await cms.find({
|
|
2051
|
+
type: MediaFile,
|
|
2052
|
+
select: {title: Query.title, url: Query.url}
|
|
2053
|
+
})
|
|
2054
|
+
// Without a select each result has its url as `_url`
|
|
2055
|
+
```
|
|
2056
|
+
|
|
2057
|
+
`ImageLink` no longer needs a type parameter when the image field has no extra fields: replace `ImageLink<undefined>` with `ImageLink`.
|
|
2017
2058
|
|
|
2018
2059
|
A workspace can set `mediaUrl` to add a prefix to its media URLs, which helps when several workspaces have files with the same name.
|
|
2019
2060
|
|
|
@@ -2067,6 +2108,8 @@ Field.richText('Body', {
|
|
|
2067
2108
|
|
|
2068
2109
|
The `RichText` renderer from `alinea/ui` always renders tables with a `<tbody>`, and the `tbody` view override is gone.
|
|
2069
2110
|
|
|
2111
|
+
Attributes that a custom extension adds to a node only reach your views and tag overrides of `RichText` as `data-*` attributes: render them as `data-*` in the extension's `renderHTML`. Content saved by 1.7 gets these attributes when it is saved again, see [Custom attributes](https://v2.alineacms.com/docs/fields/rich-text#custom-attributes).
|
|
2112
|
+
|
|
2070
2113
|
### Edit.move
|
|
2071
2114
|
|
|
2072
2115
|
```tsx
|
|
@@ -2101,9 +2144,12 @@ const handler = createHandler({
|
|
|
2101
2144
|
// Inspect or rewrite the changes, return the mutations to commit
|
|
2102
2145
|
return mutations
|
|
2103
2146
|
},
|
|
2104
|
-
afterCommit({
|
|
2105
|
-
// Pages that query content refresh on their own, revalidate your own
|
|
2106
|
-
|
|
2147
|
+
afterCommit({entries, removed}) {
|
|
2148
|
+
// Pages that query content refresh on their own, revalidate your own
|
|
2149
|
+
// caches, eg. a feed that lists products
|
|
2150
|
+
const changed = [...entries, ...removed]
|
|
2151
|
+
if (changed.some(entry => entry.type === 'Product'))
|
|
2152
|
+
revalidateTag('products', 'max')
|
|
2107
2153
|
}
|
|
2108
2154
|
})
|
|
2109
2155
|
|
|
@@ -2111,7 +2157,7 @@ export const GET = handler
|
|
|
2111
2157
|
export const POST = handler
|
|
2112
2158
|
```
|
|
2113
2159
|
|
|
2114
|
-
Errors thrown in
|
|
2160
|
+
`afterCommit` receives the `mutations`, the `sha` of the new commit, the `user`, the changed `entries` as they are after the commit and the `removed` entries with the url they had. Errors thrown in it are logged and don't fail the commit. See [Instant publishing](https://v2.alineacms.com/docs/instant-publishing).
|
|
2115
2161
|
|
|
2116
2162
|
### remote is replaced by backend
|
|
2117
2163
|
|
|
@@ -2147,7 +2193,7 @@ The dashboard was rebuilt on React Aria Components. Nothing changes for editors
|
|
|
2147
2193
|
|
|
2148
2194
|
### Components
|
|
2149
2195
|
|
|
2150
|
-
`alinea/ui` now only exports the `RichText` renderer and the `imageBlurUrl` helper (and the deprecated `HStack`/`VStack`). Its `Button`, `Chip`, `Icon`, `Loader`, `Typo` and other components were removed, as were the files behind deep imports like `alinea/ui/Button`. Use the public component library instead, it is what the dashboard itself is built from:
|
|
2196
|
+
`alinea/ui` now only exports the `RichText` renderer and the `imageBlurUrl` helper (and the deprecated `HStack`/`VStack`, use `Stack` from `alinea/components`). Its `Button`, `Chip`, `Icon`, `Loader`, `Typo` and other components were removed, as were the files behind deep imports like `alinea/ui/Button`. Use the public component library instead, it is what the dashboard itself is built from:
|
|
2151
2197
|
|
|
2152
2198
|
```tsx
|
|
2153
2199
|
// 1.7
|
|
@@ -2159,6 +2205,23 @@ import {Button, Spinner} from 'alinea/components'
|
|
|
2159
2205
|
|
|
2160
2206
|
See [Components](https://v2.alineacms.com/docs/components). Dashboard icons are available from `alinea/dashboard/icons`.
|
|
2161
2207
|
|
|
2208
|
+
### Theme tokens
|
|
2209
|
+
|
|
2210
|
+
The CSS custom properties of 1.7 were renamed and many were removed. Styles that read an old token silently fall back to the property's default. The closest replacements:
|
|
2211
|
+
|
|
2212
|
+
- `--alinea-background` → `--alinea-bg`, `--alinea-content` → `--alinea-backdrop`, `--alinea-lift`, `--alinea-inner` and `--alinea-toolbar` → `--alinea-bg-muted`.
|
|
2213
|
+
- `--alinea-foreground` → `--alinea-fg`, `--alinea-foreground-faded` → `--alinea-fg-muted`, `--alinea-title-foreground` → `--alinea-fg-strong`.
|
|
2214
|
+
- `--alinea-outline` and `--alinea-inner-outline` → `--alinea-border`, `--alinea-highlight` → `--alinea-muted`.
|
|
2215
|
+
- `--alinea-fields` → `--alinea-input`, `--alinea-fields-foreground` → `--alinea-field-label`, `--alinea-fields-focus` and `--alinea-focus-foreground` → `--alinea-ring`.
|
|
2216
|
+
- `--alinea-selected` stays, `--alinea-selected-foreground` → `--alinea-selected-fg`, `--alinea-selected-faded` → `--alinea-primary-subtle`.
|
|
2217
|
+
- `--alinea-button-background` → `--alinea-primary`, `--alinea-button-foreground` → `--alinea-primary-fg`, `--alinea-error` → `--alinea-danger`.
|
|
2218
|
+
- `--alinea-accent` (the workspace color) → `--alinea-workspace-color`.
|
|
2219
|
+
- `--alinea-variant-info-*` → `--alinea-primary-subtle` and `--alinea-primary-subtle-fg`, `--alinea-variant-success-*` → `--alinea-success-subtle(-fg)`, `--alinea-variant-progress-*` → `--alinea-warning-subtle(-fg)`.
|
|
2220
|
+
- `--alinea-modal-background` → `--alinea-scrim`, `--alinea-shadow` → `--alinea-shadow-raised`, `--alinea-border-radius` → `--alinea-radius`.
|
|
2221
|
+
- Removed without a replacement: `--alinea-hue`, `--alinea-saturation`, `--alinea-positive`, `--alinea-negative`, `--alinea-button-hover`, `--alinea-accent-foreground`, `--alinea-divider-top`, `--alinea-divider-bottom`, `--alinea-tabs-trigger-*`, `--alinea-fields-table`, `--alinea-fields-header`, `--alinea-fields-shadow`, `--alinea-fields-selected` and the `create` and `disabled` variants. Derive hover and disabled shades with `color-mix()` and `opacity`.
|
|
2222
|
+
|
|
2223
|
+
The tokens of `alinea/css`, and its resets, now apply inside elements with a `data-alinea-theme` attribute only. The dashboard sets it on `<html>`, so the CSS of your dashboard views reads the tokens as before. A page of your site that imports `alinea/css` to render `alinea/components` needs an element with `data-alinea-theme` around them. See [Theme tokens](https://v2.alineacms.com/docs/components#theme-tokens) for the full list.
|
|
2224
|
+
|
|
2162
2225
|
### Custom field views
|
|
2163
2226
|
|
|
2164
2227
|
Field views are still referenced by path (`view: '@/fields/Range.view'`) or passed as a component. The hooks and helpers for custom views now come from one entry point, `alinea/cms`. From `alinea/dashboard`, `useField` and its related hooks still work but are deprecated: the new `useField` returns a `[value, setValue]` tuple, and the options and error have their own hooks:
|
|
@@ -2175,11 +2238,11 @@ const options = useFieldOptions(field)
|
|
|
2175
2238
|
const error = useFieldError(field)
|
|
2176
2239
|
```
|
|
2177
2240
|
|
|
2178
|
-
`useFieldValue`, `useFieldOptions`, `useFieldError` and `useFieldKey` move to `alinea/cms` too and `useFieldMutator` becomes `useFieldSetter`. Deep imports of dashboard hooks move as well: `useLocale` and `useGraph` from `alinea/dashboard/hook/*` are exported from `alinea/cms`, and `useEntryEditor` is replaced by `useEntry`. `alinea/dashboard/hooks` is internal now. `InputLabel` is now an alias of the `Field` component: it takes `label`, `description`, `error`, `required`, `disabled`, `readOnly`, `icon` and `shared`. Its 1.7 props `help`, `width`, `inline`, `size` and the fold props are gone, so help text passed through `{...options}` is no longer shown: pass it as `description`, or replace it with `FieldChrome` from `alinea/cms`, which reads the label, help text and error from the field. The form atoms (`useForm`, `FormProvider`, `FormRow` and so on) were removed from `alinea/dashboard`. See [Custom fields](https://v2.alineacms.com/docs/custom-fields) for a complete example.
|
|
2241
|
+
`useFieldValue`, `useFieldOptions`, `useFieldError` and `useFieldKey` move to `alinea/cms` too and `useFieldMutator` becomes `useFieldSetter`. Deep imports of dashboard hooks move as well: `useLocale` and `useGraph` from `alinea/dashboard/hook/*` are exported from `alinea/cms`, and `useEntryEditor` is replaced by `useEntry`. `useEntryEditor` still works as a shim for views that only read the entry: its `activeVersion` has the `id`, `locale`, `path`, `status`, `title` and `type` of the entry, and `activeVersion.data` only has `path`. Reading another property throws, read field values with `useFieldValue(field)` instead. `alinea/dashboard/hooks` is internal now. `InputLabel` is now an alias of the `Field` component: it takes `label`, `description`, `error`, `required`, `disabled`, `readOnly`, `icon` and `shared`. Its 1.7 props `help`, `width`, `inline`, `size` and the fold props are gone, so help text passed through `{...options}` is no longer shown: pass it as `description`, or replace it with `FieldChrome` from `alinea/cms`, which reads the label, help text and error from the field. The form atoms (`useForm`, `FormProvider`, `FormRow` and so on) were removed from `alinea/dashboard`. See [Custom fields](https://v2.alineacms.com/docs/custom-fields) for a complete example.
|
|
2179
2242
|
|
|
2180
2243
|
### Custom type views
|
|
2181
2244
|
|
|
2182
|
-
A type's `view` now only receives `{type}`, the `editor` prop of 1.7 is gone. A custom `auth` view in the config is not used: the dashboard always renders its own sign-in.
|
|
2245
|
+
A type's `view` now only receives `{type}`, the `editor` prop of 1.7 is gone. The view no longer replaces the whole editor: it renders below the entry header, which publishes and saves drafts, and reads and writes the fields of the entry with the hooks of `alinea/cms`. A custom `auth` view in the config is not used: the dashboard always renders its own sign-in.
|
|
2183
2246
|
|
|
2184
2247
|
### List overviews
|
|
2185
2248
|
|
|
@@ -2226,6 +2289,24 @@ See [Overviews](https://v2.alineacms.com/docs/overviews) for sorting, custom cel
|
|
|
2226
2289
|
|
|
2227
2290
|
`alinea/yjs` and `alinea/cloud/view/CloudAuth` no longer exist. `alinea/picker/entry/EntryPicker` and `alinea/picker/url/UrlPicker` no longer ship the picker UI: `EntryPickerModal`, `UrlPickerForm` and `UrlPickerModal` were removed without a public replacement. Import the picker option types (`EditorInfo`, `EditorLocation`, `EditorLimitLocation` and `EntryPickerConditions`) from `alinea/field/link`. Code that subclassed fields relied on the removed shape system (`alinea/core/Shape`, `Field.shape`, `Type.shape`): the field constructors no longer take a shape, and `postProcess` became `queryValue`.
|
|
2228
2291
|
|
|
2292
|
+
## From a 2.0 preview
|
|
2293
|
+
|
|
2294
|
+
Projects on a 2.0 preview have most of the above already. These changes landed in the release candidate:
|
|
2295
|
+
|
|
2296
|
+
- Image links no longer have `url`, use `src`. File links keep `url` and `href`.
|
|
2297
|
+
- `useEntry()` from `alinea/cms` returns a `DashboardEntry`: the `id`, `type`, `title`, `path`, `url`, `locale`, `status`, `workspace`, `root` and `parentId` of the entry and its saved field values in `data`. The other properties of the internal entry row are gone.
|
|
2298
|
+
- `useEntryEditor()`: the publish status moved from `activeVersion.data.status` to `activeVersion.status`. Reading another property of `activeVersion.data` than `path` throws.
|
|
2299
|
+
- `useSiblingFieldValue(key)` is deprecated: pass the field to `useFieldValue(field)`, which is typed and also reads fields of a parent row or the entry, or its key: `useFieldValue('path')`. `EditFields` takes an object field to render its fields in its scope.
|
|
2300
|
+
- Theme tokens apply inside `data-alinea-theme` only, see [Theme tokens](#theme-tokens) above. The workspace color is `--alinea-workspace-color`.
|
|
2301
|
+
- `Select` from `alinea/components` only shows its clear button with `clearable`. `Field.select` still lets editors clear fields that aren't required.
|
|
2302
|
+
- `Select`, `ComboBox` and `MultipleSelect` values are strings or numbers, and `onValueChange` is typed like the `value` you pass. Without a `value`, it receives `string | number`.
|
|
2303
|
+
- `TimeField` with a `name` submits `14:30` (or `14:30:15` with `granularity="second"`) like a native time input, it used to submit seconds always.
|
|
2304
|
+
- `NumberField` no longer groups digits: pass `formatOptions={{useGrouping: true}}` to show `1,250`.
|
|
2305
|
+
- `afterCommit` also receives the changed `entries` and the `removed` entries, so hooks no longer have to look them up from the mutations.
|
|
2306
|
+
- Every field accepts `width`, custom fields don't need to declare it in their options.
|
|
2307
|
+
- `alinea build` accepts an absolute `handlerUrl` without a production `baseUrl`, and its error names the missing `baseUrl` setting.
|
|
2308
|
+
- A type's `view` renders below the entry header, in the editor of the entry: the field hooks of `alinea/cms` and `EditField` edit its fields and the header publishes them. It used to replace the whole editor without access to the fields.
|
|
2309
|
+
|
|
2229
2310
|
## After upgrading
|
|
2230
2311
|
|
|
2231
2312
|
Run your build and open the dashboard. Things to try: an image renders on your site through `/admin/file/...`, publishing an entry updates the site, and custom fields still render. Then have a look at what's new in 2.0: [Field.localiser](https://v2.alineacms.com/docs/internationalization) for per-field translations, URL aliases and backlinks, [instant publishing](https://v2.alineacms.com/docs/instant-publishing) and the [MCP server](https://v2.alineacms.com/docs/mcp) for coding agents.
|
|
@@ -2403,6 +2484,7 @@ The label ("Author") is what editors see. The name the type is stored under is i
|
|
|
2403
2484
|
## Options
|
|
2404
2485
|
|
|
2405
2486
|
- `fields` (required): the [fields](https://v2.alineacms.com/docs/fields) of the type, keyed by name. Field names must start with a letter and contain only letters, digits and underscores. Spread [tabs](https://v2.alineacms.com/docs/fields/tabs) or `Field.view(...)` sections in between to lay out the form.
|
|
2487
|
+
- `settings`: fields that editors set in the settings sheet of a block, below its label and anchor, instead of in the block's form. Entries of the type show them in a Settings tab. They are stored and queried like the other fields, so their names can't repeat a field name.
|
|
2406
2488
|
- `contains`: the types that can be created as children of this entry, by schema name or by reference. Without it, entries of this type can't have children.
|
|
2407
2489
|
- `overview`: how the dashboard lists the children: columns, the default order (`sort`), the layout and actions. See [Overviews](https://v2.alineacms.com/docs/overviews).
|
|
2408
2490
|
- `insertOrder`: where new children are added: `'first'`, `'last'` or `'free'` (default), which lets the editor choose in the create dialog.
|
|
@@ -2411,7 +2493,8 @@ The label ("Author") is what editors see. The name the type is stored under is i
|
|
|
2411
2493
|
- `hidden`: set to `true` to leave entries of this type out of the sidebar tree. They can still be linked to and queried.
|
|
2412
2494
|
- `defaultView`: `'edit'` opens the form when an entry is selected, `'overview'` shows a table of its children first. The default is `'overview'` for entries that have children and `'edit'` otherwise.
|
|
2413
2495
|
- `preview`: turn [live previews](https://v2.alineacms.com/docs/live-previews) on or off for this type (`true`/`false`), or pass a React component that renders the preview. It overrides the setting of the root, workspace and config.
|
|
2414
|
-
- `view`: a React component, or a path to one, that replaces the
|
|
2496
|
+
- `view`: a React component, or a path to one, that replaces the form of entries of this type. It renders below the entry header and edits the entry with the hooks of `alinea/cms`. It receives `{type}`. See [Replace the form of a type](https://v2.alineacms.com/docs/custom-fields#replace-the-form-of-a-type).
|
|
2497
|
+
- `actions`: components rendered in the header of the entry editor, next to the publish button, such as an export button. Each is a component or a path to one and receives `EntryActionProps`: the `entry` and the `locale` it is edited in. See [Add actions to the entry header](https://v2.alineacms.com/docs/custom-fields#add-actions-to-the-entry-header).
|
|
2415
2498
|
- `orderChildrenBy` (deprecated): sort children by one or more fields. Use `overview.sort`, which still accepts the same value.
|
|
2416
2499
|
- `summaryRow`, `summaryThumb` (deprecated): not used by the dashboard. Configure the columns and card image in the `overview` of the parent instead.
|
|
2417
2500
|
|
|
@@ -2572,6 +2655,8 @@ export const Home = Config.document('Home', {
|
|
|
2572
2655
|
})
|
|
2573
2656
|
```
|
|
2574
2657
|
|
|
2658
|
+
Next to `fields`, a document takes `seo` and `details`: fields added to the SEO and Details tabs. `settings` adds a Settings tab with its fields.
|
|
2659
|
+
|
|
2575
2660
|
## Using the metadata
|
|
2576
2661
|
|
|
2577
2662
|
Select the metadata field in the query for a page and map it to your framework's head tags. With the Next.js App Router:
|
|
@@ -3155,6 +3240,37 @@ export function Prose({doc}: {doc: TextDoc}) {
|
|
|
3155
3240
|
|
|
3156
3241
|
The overridable tags are `h1`–`h6`, `p`, `b`, `i`, `ul`, `ol`, `li`, `blockquote`, `hr`, `img`, `br`, `small`, `sub`, `sup`, `a`, `table`, `tr`, `td` and `th`. Pass `text` to wrap every piece of text in a component. Headings get an `id` (the anchor an editor set, or a slug of the heading text), so you can link to sections.
|
|
3157
3242
|
|
|
3243
|
+
### Custom attributes
|
|
3244
|
+
|
|
3245
|
+
Attributes that a custom extension adds to a node are stored with the node, but only the `data-*` attributes its `renderHTML` produces reach the rendered elements and your tag overrides. Render an attribute as `data-*` to use it on your site:
|
|
3246
|
+
|
|
3247
|
+
```tsx
|
|
3248
|
+
import {Field} from 'alinea'
|
|
3249
|
+
import {extensions} from 'alinea/field/richtext'
|
|
3250
|
+
|
|
3251
|
+
const Checklist = extensions.BulletList.extend({
|
|
3252
|
+
addAttributes() {
|
|
3253
|
+
return {
|
|
3254
|
+
...this.parent?.(),
|
|
3255
|
+
checklist: {
|
|
3256
|
+
default: null,
|
|
3257
|
+
renderHTML: attributes =>
|
|
3258
|
+
attributes.checklist ? {'data-checklist': 'true'} : {}
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
})
|
|
3263
|
+
|
|
3264
|
+
export const body = Field.richText('Body', {
|
|
3265
|
+
extensions: defaults => ({...defaults, BulletList: Checklist})
|
|
3266
|
+
})
|
|
3267
|
+
|
|
3268
|
+
// <RichText doc={body} ul={<ul className="list" />} /> renders
|
|
3269
|
+
// <ul data-checklist="true" class="list">
|
|
3270
|
+
```
|
|
3271
|
+
|
|
3272
|
+
Content saved by Alinea 1.x only has the raw attribute (`checklist: true`). Its `data-*` attribute is added when the entry is saved again in the dashboard, until then views don't receive it.
|
|
3273
|
+
|
|
3158
3274
|
### Blocks
|
|
3159
3275
|
|
|
3160
3276
|
Blocks are rendered only when you pass a component for their type name. The component receives the block's fields as props:
|
|
@@ -3304,6 +3420,25 @@ export default Config.type("List field", {
|
|
|
3304
3420
|
|
|
3305
3421
|
Row types are ordinary [Types](https://v2.alineacms.com/docs/schema/type): they can have tabs, an `icon` that is shown in the add menu and nested lists. They don't need a title or path and don't have to be in the schema.
|
|
3306
3422
|
|
|
3423
|
+
Options that change how a block looks rather than what it says, such as a background or a layout variant, can go in the type's `settings`. Editors find them in the block's settings sheet, below the label and anchor, which keeps the block form short. They're stored on the row next to its other fields:
|
|
3424
|
+
|
|
3425
|
+
```tsx
|
|
3426
|
+
const HeroBlock = Config.type('Hero', {
|
|
3427
|
+
fields: {
|
|
3428
|
+
title: Field.text('Title'),
|
|
3429
|
+
text: Field.richText('Text')
|
|
3430
|
+
},
|
|
3431
|
+
settings: {
|
|
3432
|
+
background: Field.select('Background', {
|
|
3433
|
+
options: {light: 'Light', dark: 'Dark'},
|
|
3434
|
+
initialValue: 'light'
|
|
3435
|
+
})
|
|
3436
|
+
}
|
|
3437
|
+
})
|
|
3438
|
+
|
|
3439
|
+
// A queried row: {_type: 'HeroBlock', title, text, background, ...}
|
|
3440
|
+
```
|
|
3441
|
+
|
|
3307
3442
|
## Value
|
|
3308
3443
|
|
|
3309
3444
|
The value is an array of rows. Next to its fields, every row has:
|
|
@@ -3807,11 +3942,11 @@ An object with a key per field: `{street: '', zip: '', city: ''}`. New entries s
|
|
|
3807
3942
|
|
|
3808
3943
|
## Querying
|
|
3809
3944
|
|
|
3810
|
-
|
|
3945
|
+
Without a `select` the object is returned as a whole. Select or sort on a field inside it through the object, `Venue.address.city`, and filter on inner values with `has`:
|
|
3811
3946
|
|
|
3812
3947
|
```tsx
|
|
3813
3948
|
import {cms} from '@/cms'
|
|
3814
|
-
import {Config, Field} from 'alinea'
|
|
3949
|
+
import {Config, Field, Query} from 'alinea'
|
|
3815
3950
|
|
|
3816
3951
|
export const Venue = Config.document('Venue', {
|
|
3817
3952
|
fields: {
|
|
@@ -3827,8 +3962,10 @@ export const Venue = Config.document('Venue', {
|
|
|
3827
3962
|
const venuesInGhent = await cms.find({
|
|
3828
3963
|
type: Venue,
|
|
3829
3964
|
filter: {address: {has: {city: 'Ghent'}}},
|
|
3830
|
-
select: {
|
|
3965
|
+
select: {title: Query.title, street: Venue.address.street},
|
|
3966
|
+
orderBy: {asc: Venue.address.street}
|
|
3831
3967
|
})
|
|
3968
|
+
// Array<{title: string, street: string}>
|
|
3832
3969
|
```
|
|
3833
3970
|
|
|
3834
3971
|
## Reusing a group of fields
|
|
@@ -4304,7 +4441,6 @@ The picker only offers images: files with an extension such as `.jpg`, `.png`, `
|
|
|
4304
4441
|
Each image is resolved to an `ImageLink`:
|
|
4305
4442
|
|
|
4306
4443
|
- `src`: the url of the image on your site, such as `/admin/file/landscape.jpg?v=3f2a…`. It's served through your Alinea handler, and the `v` parameter changes when the file is replaced, so caches never serve an outdated copy.
|
|
4307
|
-
- `url`: the same as `src`.
|
|
4308
4444
|
- `width` and `height`: the dimensions in pixels.
|
|
4309
4445
|
- `alt`: the alt text editors enter in the media library, in the locale of the entry you queried when the media root is [translated](https://v2.alineacms.com/docs/workspaces/media).
|
|
4310
4446
|
- `focus`: the focal point as `{x, y}`, each between 0 and 1. Use it as `object-position` when you crop the image.
|
|
@@ -4759,7 +4895,58 @@ const urls = await cms.find({type: BlogPost, select: Query.url})
|
|
|
4759
4895
|
// Array<string>
|
|
4760
4896
|
```
|
|
4761
4897
|
|
|
4762
|
-
`Query` has `id`, `title`, `type`, `index`, `workspace`, `root`, `status`, `parentId`, `locale`, `path`, `url`, `aliases`, `createdAt` and `updatedAt`, plus the relation helpers such as `Query.children` described in [Related content](https://v2.alineacms.com/docs/query/related) and `Query.snippet` for [search results](https://v2.alineacms.com/docs/query/filtering).
|
|
4898
|
+
`Query` has `id`, `title`, `type`, `index`, `workspace`, `root`, `status`, `parentId`, `locale`, `path`, `url`, `aliases`, `createdAt` and `updatedAt`, plus the relation helpers such as `Query.children` and `Query.referencedBy` described in [Related content](https://v2.alineacms.com/docs/query/related) and `Query.snippet` for [search results](https://v2.alineacms.com/docs/query/filtering).
|
|
4899
|
+
|
|
4900
|
+
### Query fields and type fields
|
|
4901
|
+
|
|
4902
|
+
There are two kinds of references you can select. `Query.*` are the built-in properties every entry has, whatever its type: `Query.id`, `Query.title`, `Query.url` and the others above. The fields you define are selected through their type: `BlogPost.intro` is the `intro` field of `BlogPost`, `BlogPost.cover` its cover image. Type fields are typed from their definition and resolved like they are without a select, so `BlogPost.cover` returns an `ImageLink`. Combine both in one `select`:
|
|
4903
|
+
|
|
4904
|
+
```tsx
|
|
4905
|
+
import {cms} from '@/cms'
|
|
4906
|
+
import {BlogPost} from '@/schema'
|
|
4907
|
+
import {Query} from 'alinea'
|
|
4908
|
+
|
|
4909
|
+
const posts = await cms.find({
|
|
4910
|
+
type: BlogPost,
|
|
4911
|
+
select: {
|
|
4912
|
+
// Built-in properties of the entry
|
|
4913
|
+
id: Query.id,
|
|
4914
|
+
title: Query.title,
|
|
4915
|
+
url: Query.url,
|
|
4916
|
+
// Fields of the BlogPost type
|
|
4917
|
+
intro: BlogPost.intro,
|
|
4918
|
+
cover: BlogPost.cover
|
|
4919
|
+
}
|
|
4920
|
+
})
|
|
4921
|
+
// Array<{id: string, title: string, url: string, intro: string, cover: ImageLink | null}>
|
|
4922
|
+
```
|
|
4923
|
+
|
|
4924
|
+
A type field only has a value on entries of that type, so select it in a query for that `type` (or one filtered on `_type`). `Query` fields work in any query, also one without a `type`, such as a search across all entries. Fields that `Config.document` adds, like `title` and `path`, are available both ways: `Query.title` and `BlogPost.title` return the same value.
|
|
4925
|
+
|
|
4926
|
+
Fields inside an [object field](https://v2.alineacms.com/docs/fields/object) are selected through the object: `Page.seo.title` returns the `title` of the `seo` object, typed like the field. That works wherever a field can be selected, also inside relations such as `Query.children`, and in `orderBy`:
|
|
4927
|
+
|
|
4928
|
+
```tsx
|
|
4929
|
+
import {cms} from '@/cms'
|
|
4930
|
+
import {Config, Field, Query} from 'alinea'
|
|
4931
|
+
|
|
4932
|
+
export const Page = Config.document('Page', {
|
|
4933
|
+
fields: {
|
|
4934
|
+
seo: Field.object('SEO', {
|
|
4935
|
+
fields: {
|
|
4936
|
+
title: Field.text('SEO title'),
|
|
4937
|
+
description: Field.text('SEO description')
|
|
4938
|
+
}
|
|
4939
|
+
})
|
|
4940
|
+
}
|
|
4941
|
+
})
|
|
4942
|
+
|
|
4943
|
+
const pages = await cms.find({
|
|
4944
|
+
type: Page,
|
|
4945
|
+
select: {url: Query.url, title: Page.seo.title},
|
|
4946
|
+
orderBy: {asc: Page.seo.title}
|
|
4947
|
+
})
|
|
4948
|
+
// Array<{url: string, title: string}>
|
|
4949
|
+
```
|
|
4763
4950
|
|
|
4764
4951
|
Selecting only what a page needs keeps the data you pass to client components small, and TypeScript infers the result either way. To name the type of a result, use `Awaited<ReturnType<typeof yourQueryFunction>>`.
|
|
4765
4952
|
|
|
@@ -5004,6 +5191,7 @@ A filter is an object with a condition per key. The keys are:
|
|
|
5004
5191
|
|
|
5005
5192
|
- the fields of the queried `type`, such as `featured` or `publishDate`. TypeScript checks them when you pass a `type`.
|
|
5006
5193
|
- the entry's own properties with an underscore: `_id`, `_type`, `_parentId`, `_path`, `_url`, `_status`, `_locale`, `_workspace`, `_root`, `_index`, `_createdAt` and `_updatedAt`.
|
|
5194
|
+
- `_referenced`: whether another entry links to the entry. `true` or `false`, or a `Query.referencedBy(...)` query that the linking entry has to match, see [Entries that link here](https://v2.alineacms.com/docs/query/related#entries-that-link-here).
|
|
5007
5195
|
|
|
5008
5196
|
Every key has to match, so several keys in one filter mean "and".
|
|
5009
5197
|
|
|
@@ -5135,6 +5323,7 @@ export async function searchSite(terms: string, locale: string) {
|
|
|
5135
5323
|
How it matches:
|
|
5136
5324
|
|
|
5137
5325
|
- The input is split into words, and an entry must match every word.
|
|
5326
|
+
- Hyphenated, spaced and joined spellings find each other: `multi-modal`, `multi modal` and `multimodal` all match "multi-modal", "multi modal" and "multimodal".
|
|
5138
5327
|
- Words match as prefixes, so `alin` finds "Alinea".
|
|
5139
5328
|
- Small typos are tolerated: one wrong letter for words of 5 to 14 characters, more for longer words. Case and accents don't matter.
|
|
5140
5329
|
- Entry titles are always searched. Other fields only when you mark them `searchable: true`: [text](https://v2.alineacms.com/docs/fields/text) and [rich text](https://v2.alineacms.com/docs/fields/rich-text) fields support it, also inside lists, objects and rich text blocks.
|
|
@@ -5143,7 +5332,7 @@ How it matches:
|
|
|
5143
5332
|
`Query.snippet(start, end, cutOff, limit)` returns a short excerpt of the searchable text around the matches, with matches wrapped in `start` and `end`. The defaults are `'<mark>'`, `'</mark>'`, `'...'` and 64, and `limit` (the length in words) can be at most 64. It can only be used together with `search`. The excerpt is plain text from your content: escape it, or render the markers yourself instead of using `dangerouslySetInnerHTML`.
|
|
5144
5333
|
|
|
5145
5334
|
### Related (https://v2.alineacms.com/docs/query/related)
|
|
5146
|
-
A single query can also fetch entries related to the ones it finds: their parents, children and siblings, their translations, and the entries
|
|
5335
|
+
A single query can also fetch entries related to the ones it finds: their parents, children and siblings, their translations, the entries they link to and the entries that link to them. Put a relation in `select` (or in `include` when you don't select), and it's resolved in the same query.
|
|
5147
5336
|
|
|
5148
5337
|
```tsx
|
|
5149
5338
|
import {cms} from '@/cms'
|
|
@@ -5243,9 +5432,9 @@ const post = await cms.get({
|
|
|
5243
5432
|
|
|
5244
5433
|
Linked entries are looked up in the locale of the entry you queried, falling back to entries without a locale, so a link to an untranslated author keeps working from every translation of a post.
|
|
5245
5434
|
|
|
5246
|
-
##
|
|
5435
|
+
## Entries that link here
|
|
5247
5436
|
|
|
5248
|
-
Links are stored on the entry that links
|
|
5437
|
+
Links are stored on the entry that links. `Query.referencedBy` finds the entries that link to the entry, through any of their fields, including links in rich text. Pass `type` to only find entries of that type, and `field` to only follow one link field of it. Like the other relations it takes the query options, `count: true` and `first: true`:
|
|
5249
5438
|
|
|
5250
5439
|
```tsx
|
|
5251
5440
|
import {cms} from '@/cms'
|
|
@@ -5253,19 +5442,68 @@ import {Author, BlogPost} from '@/schema'
|
|
|
5253
5442
|
import {Query} from 'alinea'
|
|
5254
5443
|
|
|
5255
5444
|
export async function authorPage(path: string) {
|
|
5256
|
-
|
|
5445
|
+
return cms.get({
|
|
5257
5446
|
type: Author,
|
|
5258
5447
|
path,
|
|
5259
|
-
select: {
|
|
5448
|
+
select: {
|
|
5449
|
+
name: Query.title,
|
|
5450
|
+
bio: Author.bio,
|
|
5451
|
+
// The posts whose author field links to this author
|
|
5452
|
+
posts: Query.referencedBy({
|
|
5453
|
+
type: BlogPost,
|
|
5454
|
+
field: BlogPost.author,
|
|
5455
|
+
select: {title: Query.title, url: Query.url},
|
|
5456
|
+
orderBy: {desc: BlogPost.publishDate}
|
|
5457
|
+
}),
|
|
5458
|
+
// Every entry linking to the author, in any field
|
|
5459
|
+
mentions: Query.referencedBy({count: true})
|
|
5460
|
+
}
|
|
5260
5461
|
})
|
|
5261
|
-
|
|
5462
|
+
}
|
|
5463
|
+
```
|
|
5464
|
+
|
|
5465
|
+
The entries that link to a translated entry are looked up in its locale. For an entry without a locale, such as a shared author, the linking entries of every locale are returned: pass `locale` to only get those of one locale.
|
|
5466
|
+
|
|
5467
|
+
The same relation is a filter condition: `_referenced` holds for entries that an entry of the query links to. `{_referenced: true}` matches entries that anything links to and `{_referenced: false}` entries nothing links to, such as unused media files:
|
|
5468
|
+
|
|
5469
|
+
```tsx
|
|
5470
|
+
import {cms} from '@/cms'
|
|
5471
|
+
import {Author, BlogPost} from '@/schema'
|
|
5472
|
+
import {Query} from 'alinea'
|
|
5473
|
+
|
|
5474
|
+
// Authors of at least one featured post
|
|
5475
|
+
const featuredAuthors = await cms.find({
|
|
5476
|
+
type: Author,
|
|
5477
|
+
filter: {
|
|
5478
|
+
_referenced: Query.referencedBy({
|
|
5479
|
+
type: BlogPost,
|
|
5480
|
+
field: BlogPost.author,
|
|
5481
|
+
filter: {featured: true}
|
|
5482
|
+
})
|
|
5483
|
+
},
|
|
5484
|
+
select: Query.title
|
|
5485
|
+
})
|
|
5486
|
+
```
|
|
5487
|
+
|
|
5488
|
+
### Filtering on the link
|
|
5489
|
+
|
|
5490
|
+
The other way around, "the posts by this author" is a filter on the posts: links store the id of their target as `_entry`. This is the way to go when you start from the posts, for example to page through them:
|
|
5491
|
+
|
|
5492
|
+
```tsx
|
|
5493
|
+
import {cms} from '@/cms'
|
|
5494
|
+
import {BlogPost} from '@/schema'
|
|
5495
|
+
import {Query} from 'alinea'
|
|
5496
|
+
|
|
5497
|
+
export async function postsBy(authorId: string, page: number) {
|
|
5498
|
+
return cms.find({
|
|
5262
5499
|
type: BlogPost,
|
|
5263
5500
|
// Single link: has, multiple links: includes
|
|
5264
|
-
filter: {author: {has: {_entry:
|
|
5501
|
+
filter: {author: {has: {_entry: authorId}}},
|
|
5265
5502
|
select: {title: Query.title, url: Query.url},
|
|
5266
|
-
orderBy: {desc: BlogPost.publishDate}
|
|
5503
|
+
orderBy: {desc: BlogPost.publishDate},
|
|
5504
|
+
skip: page * 10,
|
|
5505
|
+
take: 10
|
|
5267
5506
|
})
|
|
5268
|
-
return {author, posts}
|
|
5269
5507
|
}
|
|
5270
5508
|
```
|
|
5271
5509
|
|
|
@@ -5790,6 +6028,8 @@ Diagram: Publish flow: the dashboard publishes to your site's handler, which com
|
|
|
5790
6028
|
|
|
5791
6029
|
## Skip builds for content-only commits
|
|
5792
6030
|
|
|
6031
|
+
Only set this up once your deployed site has a backend that syncs content at runtime: Alinea Cloud or a self-hosted backend. Without one, a deployment is how new content reaches your site, so skipping content-only builds would leave published changes offline.
|
|
6032
|
+
|
|
5793
6033
|
Because content is synced at runtime, a commit that only changes content does not need a new deployment. On Vercel you can skip those builds with an `ignoreCommand` in `vercel.json`. The command exits successfully, which tells Vercel to skip the build, when nothing outside the `content` directory changed since the last deployment. Comparing against `VERCEL_GIT_PREVIOUS_SHA` covers every commit in a push, not only the last one; on the first deployment of a branch it falls back to the previous commit:
|
|
5794
6034
|
|
|
5795
6035
|
File: vercel.json
|
|
@@ -5803,9 +6043,28 @@ Note (info): Adjust `content` if your content lives in a different directory.
|
|
|
5803
6043
|
|
|
5804
6044
|
## Static pages
|
|
5805
6045
|
|
|
5806
|
-
Static pages are revalidated for you. Every page that queries content is tagged, and after each commit the handler marks those pages as stale. The next visit is still answered from the cache while Next.js rebuilds the page in the background, and the visit after that gets the new content. No visitor waits for a render, and no `revalidatePath` call is needed.
|
|
6046
|
+
Static pages are revalidated for you. Every page that queries content is tagged, and after each commit the handler marks those pages as stale. The next visit is still answered from the cache while Next.js rebuilds the page in the background, and the visit after that gets the new content. No visitor waits for a render, and no `revalidatePath` call is needed. This includes pages generated with `generateStaticParams`: revalidation code in `afterCommit` that refreshes them by hand, such as `revalidateTag('_N_T_/layout')` or `revalidatePath('/', 'layout')`, can be removed.
|
|
6047
|
+
|
|
6048
|
+
Pages rendered at request time sync on their next render. The first one after a commit can still show the previous version while the shared content sha refreshes. Content pushed to your repository without the dashboard, for example a local edit, skips the handler: pages pick it up within five minutes, or within `syncInterval` seconds when your config sets it. Use the handler's `afterCommit` hook if other caches in your app depend on content, for example your own `fetch` tags. It receives the changed `entries` and the `removed` ones, see [Commit hooks](page:guides/deploy#commit-hooks):
|
|
6049
|
+
|
|
6050
|
+
File: app/(alinea)/api/cms/route.ts
|
|
6051
|
+
```tsx
|
|
6052
|
+
import {cms} from '@/cms'
|
|
6053
|
+
import {createHandler} from 'alinea/next'
|
|
6054
|
+
import {revalidateTag} from 'next/cache'
|
|
6055
|
+
|
|
6056
|
+
const handler = createHandler({
|
|
6057
|
+
cms,
|
|
6058
|
+
afterCommit({entries, removed}) {
|
|
6059
|
+
const changed = [...entries, ...removed]
|
|
6060
|
+
if (changed.some(entry => entry.type === 'Product'))
|
|
6061
|
+
revalidateTag('products', 'max')
|
|
6062
|
+
}
|
|
6063
|
+
})
|
|
5807
6064
|
|
|
5808
|
-
|
|
6065
|
+
export const GET = handler
|
|
6066
|
+
export const POST = handler
|
|
6067
|
+
```
|
|
5809
6068
|
|
|
5810
6069
|
### Editing content (https://v2.alineacms.com/docs/editing-content)
|
|
5811
6070
|
Your `cms` instance can create, update, publish and delete entries from code, for example to import content from another system or to seed a new project. Changes go through your handler, like saves in the dashboard: during development they are written to your content files.
|
|
@@ -6631,7 +6890,7 @@ Without a `backend` option the handler connects to Alinea Cloud. Pass `backend`
|
|
|
6631
6890
|
|
|
6632
6891
|
### Commit hooks
|
|
6633
6892
|
|
|
6634
|
-
Two hooks on the handler run for every commit, whatever it contains. `beforeCommit` receives the changes before they are committed: inspect or rewrite them and return the mutations to commit, or return nothing to keep them as they are. `afterCommit` runs once the commit is made, with the mutations
|
|
6893
|
+
Two hooks on the handler run for every commit, whatever it contains. `beforeCommit` receives the changes before they are committed: inspect or rewrite them and return the mutations to commit, or return nothing to keep them as they are. `afterCommit` runs once the commit is made, with the mutations, the `sha` of the new commit, the changed `entries` as they are after the commit and the `removed` entries with the url they had: revalidate caches of your own, notify your team or trigger a deploy.
|
|
6635
6894
|
|
|
6636
6895
|
File: app/(alinea)/api/cms/route.ts
|
|
6637
6896
|
```tsx
|
|
@@ -6645,9 +6904,12 @@ const handler = createHandler({
|
|
|
6645
6904
|
// Inspect or rewrite the changes, return the mutations to commit
|
|
6646
6905
|
return mutations
|
|
6647
6906
|
},
|
|
6648
|
-
afterCommit({
|
|
6649
|
-
// Pages that query content refresh on their own, revalidate your own
|
|
6650
|
-
|
|
6907
|
+
afterCommit({entries, removed}) {
|
|
6908
|
+
// Pages that query content refresh on their own, revalidate your own
|
|
6909
|
+
// caches, eg. a feed that lists products
|
|
6910
|
+
const changed = [...entries, ...removed]
|
|
6911
|
+
if (changed.some(entry => entry.type === 'Product'))
|
|
6912
|
+
revalidateTag('products', 'max')
|
|
6651
6913
|
}
|
|
6652
6914
|
})
|
|
6653
6915
|
|
|
@@ -6738,6 +7000,8 @@ When two parts implement the same thing, the one listed last wins. That is how a
|
|
|
6738
7000
|
|
|
6739
7001
|
Commits are made by the owner of the token. The editor who published is added as a `Co-authored-by` trailer with their name and email. When the branch moved since the dashboard last synced, the handler syncs and retries the commit up to three times.
|
|
6740
7002
|
|
|
7003
|
+
To commit under another name, such as when your host only deploys commits of its members, pass `author: {name, email}`. GitHub links the commit to the account that owns the email. These commits are written through the REST API: GitHub does not mark them as verified, and a save usually spends three REST requests, plus one per uploaded file, where commits by the token owner spend none. Removing a file that is already gone costs one more, and when the branch moved in the meantime the save is repeated.
|
|
7004
|
+
|
|
6741
7005
|
## Authentication
|
|
6742
7006
|
|
|
6743
7007
|
### Basic authentication
|
|
@@ -7749,6 +8013,7 @@ Components are the React components the dashboard is built from, import them fro
|
|
|
7749
8013
|
- [ColorSwatchPicker](https://v2.alineacms.com/docs/components/color-swatch-picker): Picks a color from a set of swatches.
|
|
7750
8014
|
- [DropZone](https://v2.alineacms.com/docs/components/drop-zone): A target to drop files or other content on.
|
|
7751
8015
|
- [Field](https://v2.alineacms.com/docs/components/field): The label, help text and error around your own input.
|
|
8016
|
+
- [Form](https://v2.alineacms.com/docs/components/form): Submits its fields and shows their validation errors.
|
|
7752
8017
|
|
|
7753
8018
|
### Collections
|
|
7754
8019
|
- [Table](https://v2.alineacms.com/docs/components/table): Rows and columns with sorting, selection and dragging.
|
|
@@ -7782,6 +8047,7 @@ Components are the React components the dashboard is built from, import them fro
|
|
|
7782
8047
|
- [FoldIcon](https://v2.alineacms.com/docs/components/fold-icon): A chevron that turns when its section opens.
|
|
7783
8048
|
|
|
7784
8049
|
### Layout
|
|
8050
|
+
- [Stack](https://v2.alineacms.com/docs/components/stack): Lays out its children in a row or column with a gap.
|
|
7785
8051
|
- [Surface](https://v2.alineacms.com/docs/components/surface): A bordered panel for a group of content.
|
|
7786
8052
|
- [Collapsible](https://v2.alineacms.com/docs/components/collapsible): A section that opens and closes.
|
|
7787
8053
|
- [Page](https://v2.alineacms.com/docs/components/page): The header, content and footer of a dashboard page.
|
|
@@ -7826,7 +8092,7 @@ export function PriorityView({field}: FieldViewProps<PriorityField>) {
|
|
|
7826
8092
|
}
|
|
7827
8093
|
```
|
|
7828
8094
|
|
|
7829
|
-
The components are styled by the dashboard's stylesheet and theme
|
|
8095
|
+
The components are styled by the dashboard's stylesheet and theme. They are meant for code that runs inside the dashboard, but work on other pages too (see Theme tokens below).
|
|
7830
8096
|
|
|
7831
8097
|
## Conventions
|
|
7832
8098
|
|
|
@@ -7845,6 +8111,42 @@ The props of every component are typed, so your editor shows what each one accep
|
|
|
7845
8111
|
|
|
7846
8112
|
Components with an `icon` prop take an icon component (any component that renders an `<svg>`, such as those from [react-icons](https://react-icons.github.io/react-icons) or copied from [Icones](https://icones.js.org)) or an element. The icons of the dashboard itself are exported from `alinea/dashboard/icons`.
|
|
7847
8113
|
|
|
8114
|
+
## Theme tokens
|
|
8115
|
+
|
|
8116
|
+
The components are styled with CSS custom properties. `alinea/css` declares them, and its resets, only on elements with a `data-alinea-theme` attribute, so loading the stylesheet doesn't restyle the rest of a page. The dashboard sets the attribute on `<html>`, and popovers, dialogs and tooltips set it on themselves since they render outside the page. To use the components on a page of your site, import `alinea/css` and render them inside an element with `data-alinea-theme`. Add `data-theme="light"` or `data-theme="dark"` to that element to fix the color scheme, otherwise it follows the page.
|
|
8117
|
+
|
|
8118
|
+
A theme only sets the inputs, as `light-dark()` pairs where a value differs per color scheme. Every other token is derived from them. Set them on the element with `data-alinea-theme`, for example `<div data-alinea-theme className="brand-theme">`:
|
|
8119
|
+
|
|
8120
|
+
File: theme.css
|
|
8121
|
+
```css
|
|
8122
|
+
.brand-theme {
|
|
8123
|
+
--alinea-radius: 6px;
|
|
8124
|
+
--alinea-bg: light-dark(#ffffff, #18181b);
|
|
8125
|
+
--alinea-fg: light-dark(#27272a, #e4e4e7);
|
|
8126
|
+
--alinea-primary: light-dark(#2f6b45, #5fbf82);
|
|
8127
|
+
}
|
|
8128
|
+
```
|
|
8129
|
+
|
|
8130
|
+
Popovers, dialogs and tooltips render outside that element and carry the attribute themselves, so a class on the page doesn't reach them: use `[data-alinea-theme]` as the selector to theme every part of the components on a page.
|
|
8131
|
+
|
|
8132
|
+
Inputs:
|
|
8133
|
+
|
|
8134
|
+
- `--alinea-font`, `--alinea-mono-font`, `--alinea-font-size-base` and `--alinea-radius`.
|
|
8135
|
+
- `--alinea-backdrop` (behind panels), `--alinea-bg` (panels), `--alinea-fg` (text) and `--alinea-border`.
|
|
8136
|
+
- `--alinea-primary`, `--alinea-danger`, `--alinea-warning` and `--alinea-success`, with `--alinea-primary-fg`, `--alinea-danger-fg` and `--alinea-warning-fg` for text on them.
|
|
8137
|
+
- `--alinea-ring`: the focus ring.
|
|
8138
|
+
- `--alinea-workspace-color`: the `color` option of the current workspace, set by the dashboard. It falls back to `--alinea-primary`.
|
|
8139
|
+
|
|
8140
|
+
Derived tokens, to read in your own styles:
|
|
8141
|
+
|
|
8142
|
+
- Text: `--alinea-fg-strong`, `--alinea-fg-muted`, `--alinea-fg-disabled` and `--alinea-field-label`.
|
|
8143
|
+
- Surfaces: `--alinea-bg-muted`, `--alinea-muted` (hover backgrounds), `--alinea-input`, `--alinea-overlay`, `--alinea-scrim` (behind dialogs) and `--alinea-border-strong`.
|
|
8144
|
+
- Tones: `--alinea-selected` and `--alinea-selected-fg`, and a `-subtle` background with a `-subtle-fg` text color for `primary`, `danger`, `warning` and `success`, eg. `--alinea-danger-subtle`.
|
|
8145
|
+
- Entry statuses: `--alinea-status-published`, `--alinea-status-draft` and `--alinea-status-unpublished`, each with a `-fg` color.
|
|
8146
|
+
- Shape and motion: `--alinea-radius-sm`, `--alinea-radius-lg`, `--alinea-radius-xl`, `--alinea-shadow-raised`, `--alinea-shadow-muted`, `--alinea-shadow-tooltip`, `--alinea-shadow-modal` and `--alinea-transition`.
|
|
8147
|
+
|
|
8148
|
+
The color palette the default theme is built from (`--alinea-gray-500`, `--alinea-blue-800`, ...) is not part of this list and can change.
|
|
8149
|
+
|
|
7848
8150
|
### Alert (https://v2.alineacms.com/docs/components/alert)
|
|
7849
8151
|
A callout for an important message, with a title, a description and optional actions.
|
|
7850
8152
|
|
|
@@ -8669,44 +8971,133 @@ export function ComboBoxExample() {
|
|
|
8669
8971
|
}
|
|
8670
8972
|
```
|
|
8671
8973
|
|
|
8974
|
+
## Groups and descriptions
|
|
8975
|
+
|
|
8976
|
+
Group items under a label with ComboBoxGroup. Like SelectItem, a ComboBoxItem can show an `icon` and a `description` below its label. Values are strings or numbers: `onValueChange` receives the `value` of the picked item, typed like the `value` you pass.
|
|
8977
|
+
|
|
8978
|
+
Example: ComboBox: groups
|
|
8979
|
+
```tsx
|
|
8980
|
+
import {ComboBox, ComboBoxGroup, ComboBoxItem} from 'alinea/components'
|
|
8981
|
+
import {
|
|
8982
|
+
IcRoundDescription,
|
|
8983
|
+
IcRoundFeed,
|
|
8984
|
+
IcRoundImage
|
|
8985
|
+
} from 'alinea/dashboard/icons'
|
|
8986
|
+
|
|
8987
|
+
export function ComboBoxGroupsExample() {
|
|
8988
|
+
return (
|
|
8989
|
+
<ComboBox label="Link to" style={{width: 280}}>
|
|
8990
|
+
<ComboBoxGroup label="Pages">
|
|
8991
|
+
<ComboBoxItem value="about" icon={IcRoundDescription}>
|
|
8992
|
+
About us
|
|
8993
|
+
</ComboBoxItem>
|
|
8994
|
+
<ComboBoxItem
|
|
8995
|
+
value="collection"
|
|
8996
|
+
icon={IcRoundImage}
|
|
8997
|
+
description="Products of the summer collection"
|
|
8998
|
+
>
|
|
8999
|
+
Summer collection
|
|
9000
|
+
</ComboBoxItem>
|
|
9001
|
+
</ComboBoxGroup>
|
|
9002
|
+
<ComboBoxGroup label="Blog">
|
|
9003
|
+
<ComboBoxItem
|
|
9004
|
+
value="care"
|
|
9005
|
+
icon={IcRoundFeed}
|
|
9006
|
+
description="Published last week"
|
|
9007
|
+
>
|
|
9008
|
+
Caring for linen
|
|
9009
|
+
</ComboBoxItem>
|
|
9010
|
+
</ComboBoxGroup>
|
|
9011
|
+
</ComboBox>
|
|
9012
|
+
)
|
|
9013
|
+
}
|
|
9014
|
+
```
|
|
9015
|
+
|
|
9016
|
+
## Load items as you type
|
|
9017
|
+
|
|
9018
|
+
To search on a server, pass `filter={false}` so every item you render is shown, and `loading` while a request runs: it shows a spinner in the input, and in the list while it is empty. `useAsyncList` loads the items for the text in the input and cancels a request when a newer one replaces it. A function as `filter` replaces the default matching instead: `(textValue, inputValue) => boolean`.
|
|
9019
|
+
|
|
9020
|
+
Example: ComboBox: async
|
|
9021
|
+
```tsx
|
|
9022
|
+
import {ComboBox, ComboBoxItem, useAsyncList} from 'alinea/components'
|
|
9023
|
+
|
|
9024
|
+
const products = [
|
|
9025
|
+
{id: 'shirt', title: 'Linen shirt'},
|
|
9026
|
+
{id: 'chair', title: 'Oak dining chair'},
|
|
9027
|
+
{id: 'throw', title: 'Wool throw'},
|
|
9028
|
+
{id: 'table', title: 'Walnut side table'},
|
|
9029
|
+
{id: 'lamp', title: 'Ceramic table lamp'}
|
|
9030
|
+
]
|
|
9031
|
+
|
|
9032
|
+
// Stands in for a search request to your own API
|
|
9033
|
+
async function searchProducts(text: string) {
|
|
9034
|
+
await new Promise(resolve => setTimeout(resolve, 400))
|
|
9035
|
+
const query = text.toLowerCase()
|
|
9036
|
+
return products.filter(product => product.title.toLowerCase().includes(query))
|
|
9037
|
+
}
|
|
9038
|
+
|
|
9039
|
+
export function ComboBoxAsyncExample() {
|
|
9040
|
+
const list = useAsyncList({
|
|
9041
|
+
async load({filterText}) {
|
|
9042
|
+
return {items: await searchProducts(filterText)}
|
|
9043
|
+
}
|
|
9044
|
+
})
|
|
9045
|
+
return (
|
|
9046
|
+
<ComboBox
|
|
9047
|
+
label="Related product"
|
|
9048
|
+
inputValue={list.filterText}
|
|
9049
|
+
onInputValueChange={list.setFilterText}
|
|
9050
|
+
filter={false}
|
|
9051
|
+
loading={list.loading}
|
|
9052
|
+
emptyMessage="No products found"
|
|
9053
|
+
style={{width: 280}}
|
|
9054
|
+
>
|
|
9055
|
+
{list.items.map(product => (
|
|
9056
|
+
<ComboBoxItem key={product.id} value={product.id}>
|
|
9057
|
+
{product.title}
|
|
9058
|
+
</ComboBoxItem>
|
|
9059
|
+
))}
|
|
9060
|
+
</ComboBox>
|
|
9061
|
+
)
|
|
9062
|
+
}
|
|
9063
|
+
```
|
|
9064
|
+
|
|
8672
9065
|
## Props
|
|
8673
9066
|
|
|
8674
9067
|
`ComboBox`
|
|
8675
9068
|
A labelled text input that filters a list of options to pick from
|
|
8676
9069
|
| Prop | Type | Default | Description |
|
|
8677
9070
|
| --- | --- | --- | --- |
|
|
8678
|
-
| value | `
|
|
8679
|
-
| defaultValue | `
|
|
8680
|
-
| onValueChange | `(value:
|
|
9071
|
+
| value | `V \| null` | | The value of the selected ComboBoxItem |
|
|
9072
|
+
| defaultValue | `V \| null` | | |
|
|
9073
|
+
| onValueChange | `(value: V \| null) => void` | | Called with the selected value, or null when the selection is cleared |
|
|
8681
9074
|
| inputValue | `string` | | |
|
|
8682
9075
|
| defaultInputValue | `string` | | |
|
|
8683
9076
|
| onInputValueChange | `(value: string) => void` | | |
|
|
8684
9077
|
| placeholder | `string` | | |
|
|
8685
9078
|
| allowsCustomValue | `boolean` | | Keep text that does not match an item instead of reverting it on blur |
|
|
8686
9079
|
| emptyMessage | `ReactNode` | | Shown in the list when no item matches, the list stays closed if unset |
|
|
9080
|
+
| filter | `false \| ((textValue: string, inputValue: string) => boolean)` | | Whether an item is shown for the typed text, defaults to a case and accent insensitive match anywhere in its text. Pass false to show every item, eg. when the server already filtered them. |
|
|
9081
|
+
| loading | `boolean` | | Shows a spinner, eg. while the items for the typed text load |
|
|
8687
9082
|
| name | `string` | | Name of the hidden form input carrying the value |
|
|
8688
9083
|
| autoFocus | `boolean` | | |
|
|
8689
|
-
| label | `ReactNode` | | |
|
|
8690
|
-
| description | `ReactNode` | | |
|
|
8691
|
-
| error | `ReactNode` | | |
|
|
8692
|
-
| required | `boolean` | | |
|
|
8693
|
-
| disabled | `boolean` | | |
|
|
8694
|
-
| readOnly | `boolean` | | |
|
|
8695
|
-
| icon | `IconType \| ReactElement` | | |
|
|
8696
|
-
| shared | `boolean` | | Marks the field as shared between translations |
|
|
8697
|
-
| open | `boolean` | | |
|
|
8698
|
-
| defaultOpen | `boolean` | | |
|
|
8699
|
-
| onOpenChange | `(open: boolean) => void` | | |
|
|
8700
|
-
Also accepts `className` and `style`, `id` and `aria-*` labelling attributes, `data-*` attributes.
|
|
8701
9084
|
|
|
8702
9085
|
`ComboBoxItem`
|
|
8703
9086
|
| Prop | Type | Default | Description |
|
|
8704
9087
|
| --- | --- | --- | --- |
|
|
8705
|
-
| value (required) | `
|
|
9088
|
+
| value (required) | `Key` | | |
|
|
8706
9089
|
| textValue | `string` | | Text used for filtering and the input, defaults to string children |
|
|
8707
9090
|
| disabled | `boolean` | | |
|
|
9091
|
+
| icon | `IconType \| ReactElement` | | Icon shown before the label in the list |
|
|
9092
|
+
| description | `ReactNode` | | Secondary text shown below the label in the list |
|
|
8708
9093
|
Also accepts `className` and `style`.
|
|
8709
9094
|
|
|
9095
|
+
`ComboBoxGroup`
|
|
9096
|
+
| Prop | Type | Default | Description |
|
|
9097
|
+
| --- | --- | --- | --- |
|
|
9098
|
+
| label | `ReactNode` | | |
|
|
9099
|
+
Also accepts `id` and `aria-*` labelling attributes.
|
|
9100
|
+
|
|
8710
9101
|
### Command (https://v2.alineacms.com/docs/components/command)
|
|
8711
9102
|
A searchable list of commands, like a command palette.
|
|
8712
9103
|
|
|
@@ -9030,7 +9421,7 @@ export function DateFieldExample() {
|
|
|
9030
9421
|
`DateField`
|
|
9031
9422
|
| Prop | Type | Default | Description |
|
|
9032
9423
|
| --- | --- | --- | --- |
|
|
9033
|
-
| locale | `string; /** The date, `YYYY-MM-DD` */ value?: string \| null; defaultValue?: string \| null; onValueChange?: (value: string \| null) => void; /** The earliest valid date, `YYYY-MM-DD` */ min?: string; /** The latest valid date, `YYYY-MM-DD` */ max?: string; name?: string; autoFocus?: boolean;` | | BCP 47 locale used to format dates, eg. `en-GB`, defaults to the user's locale |
|
|
9424
|
+
| locale | `string; /** The date, `YYYY-MM-DD` */ value?: string \| null; defaultValue?: string \| null; onValueChange?: (value: string \| null) => void; /** The earliest valid date, `YYYY-MM-DD` */ min?: string; /** The latest valid date, `YYYY-MM-DD` */ max?: string; /** * Name of the hidden form input, which submits the date like a native * date input: `YYYY-MM-DD`, or an empty string when no date is set */ name?: string; autoFocus?: boolean;` | | BCP 47 locale used to format dates, eg. `en-GB`, defaults to the user's locale |
|
|
9034
9425
|
| label | `ReactNode` | | |
|
|
9035
9426
|
| description | `ReactNode` | | |
|
|
9036
9427
|
| error | `ReactNode` | | |
|
|
@@ -9245,11 +9636,60 @@ export function DialogFormExample() {
|
|
|
9245
9636
|
}
|
|
9246
9637
|
```
|
|
9247
9638
|
|
|
9639
|
+
## Submit a form
|
|
9640
|
+
|
|
9641
|
+
Pass `onSubmit` to DialogContent to render its content as a [Form](https://v2.alineacms.com/docs/components/form): a Button with `type="submit"` submits it with the values of the named fields. The dialog closes once `onSubmit` resolves without errors. Errors it returns, keyed by field name, show on the fields and keep the dialog open. `onClose` on Dialog runs whenever the dialog closes, after `onOpenChange(false)`, for example to reset state. The form takes no room of its own (`display: contents`): put spacing such as a `gap` on DialogContent.
|
|
9642
|
+
|
|
9643
|
+
Example: Dialog: submit
|
|
9644
|
+
```tsx
|
|
9645
|
+
import {
|
|
9646
|
+
Button,
|
|
9647
|
+
Dialog,
|
|
9648
|
+
DialogClose,
|
|
9649
|
+
DialogContent,
|
|
9650
|
+
DialogDescription,
|
|
9651
|
+
DialogFooter,
|
|
9652
|
+
DialogHeader,
|
|
9653
|
+
DialogTitle,
|
|
9654
|
+
DialogTrigger,
|
|
9655
|
+
TextField
|
|
9656
|
+
} from 'alinea/components'
|
|
9657
|
+
|
|
9658
|
+
// Stands in for a request to your own API, which returns errors by field name
|
|
9659
|
+
async function movePage(data: FormData) {
|
|
9660
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
9661
|
+
if (data.get('path') === 'about') return {path: 'This path is taken'}
|
|
9662
|
+
return undefined
|
|
9663
|
+
}
|
|
9664
|
+
|
|
9665
|
+
export function DialogSubmitExample() {
|
|
9666
|
+
return (
|
|
9667
|
+
<Dialog>
|
|
9668
|
+
<DialogTrigger variant="outline">Change path</DialogTrigger>
|
|
9669
|
+
<DialogContent onSubmit={movePage}>
|
|
9670
|
+
<DialogHeader>
|
|
9671
|
+
<DialogTitle>Change path</DialogTitle>
|
|
9672
|
+
<DialogDescription>Try “about” to see an error.</DialogDescription>
|
|
9673
|
+
</DialogHeader>
|
|
9674
|
+
<TextField label="Path" name="path" defaultValue="about" autoFocus />
|
|
9675
|
+
<DialogFooter>
|
|
9676
|
+
<DialogClose variant="ghost">Cancel</DialogClose>
|
|
9677
|
+
<Button type="submit" color="primary">
|
|
9678
|
+
Save
|
|
9679
|
+
</Button>
|
|
9680
|
+
</DialogFooter>
|
|
9681
|
+
</DialogContent>
|
|
9682
|
+
</Dialog>
|
|
9683
|
+
)
|
|
9684
|
+
}
|
|
9685
|
+
```
|
|
9686
|
+
|
|
9248
9687
|
## Props
|
|
9249
9688
|
|
|
9250
9689
|
`Dialog`
|
|
9251
9690
|
| Prop | Type | Default | Description |
|
|
9252
9691
|
| --- | --- | --- | --- |
|
|
9692
|
+
| onClose | `() => void` | | Called when the dialog closes, after `onOpenChange(false)` |
|
|
9253
9693
|
| open | `boolean` | | |
|
|
9254
9694
|
| defaultOpen | `boolean` | | |
|
|
9255
9695
|
| onOpenChange | `(open: boolean) => void` | | |
|
|
@@ -9264,6 +9704,8 @@ Accepts every prop of `Button`.
|
|
|
9264
9704
|
| size | `'default' \| 'lg' \| 'full'` | `'default'` | `default` fits its content up to a narrow width, `lg` is a fixed wide dialog and `full` fills the viewport (minus a margin), defaults to `default` |
|
|
9265
9705
|
| dismissable | `boolean` | `true` | Close the dialog when clicking outside of it, defaults to true |
|
|
9266
9706
|
| showCloseButton | `boolean` | `true` | Defaults to true |
|
|
9707
|
+
| onSubmit | `FormProps['onSubmit']` | | Renders the content as a Form, submitted by a `type="submit"` Button. The dialog closes once this resolves without returning errors, which are shown on the fields otherwise. |
|
|
9708
|
+
| validationErrors | `ValidationErrors` | | Errors keyed by field name, when the content is a form |
|
|
9267
9709
|
Also accepts `className` and `style`, `id` and `aria-*` labelling attributes, `data-*` attributes.
|
|
9268
9710
|
|
|
9269
9711
|
`DialogHeader`
|
|
@@ -9774,13 +10216,80 @@ export function FoldIconExample() {
|
|
|
9774
10216
|
| --- | --- | --- | --- |
|
|
9775
10217
|
| expanded | `boolean` | | |
|
|
9776
10218
|
|
|
9777
|
-
###
|
|
9778
|
-
A
|
|
10219
|
+
### Form (https://v2.alineacms.com/docs/components/form)
|
|
10220
|
+
A form whose fields show their validation errors. `onSubmit` receives the `FormData` of the named fields instead of the browser submitting the form. It can return errors keyed by field name, also from a promise, which show on those fields until they are edited.
|
|
9779
10221
|
|
|
9780
|
-
Example:
|
|
10222
|
+
Example: Form
|
|
9781
10223
|
```tsx
|
|
9782
|
-
import {
|
|
9783
|
-
|
|
10224
|
+
import {
|
|
10225
|
+
Button,
|
|
10226
|
+
Form,
|
|
10227
|
+
Select,
|
|
10228
|
+
SelectItem,
|
|
10229
|
+
Stack,
|
|
10230
|
+
Text,
|
|
10231
|
+
TextField
|
|
10232
|
+
} from 'alinea/components'
|
|
10233
|
+
import {useState} from 'react'
|
|
10234
|
+
|
|
10235
|
+
// Stands in for a request to your own API, which returns errors by field name
|
|
10236
|
+
async function createPage(data: FormData) {
|
|
10237
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
10238
|
+
if (data.get('path') === 'about') return {path: 'This path is taken'}
|
|
10239
|
+
return undefined
|
|
10240
|
+
}
|
|
10241
|
+
|
|
10242
|
+
export function FormExample() {
|
|
10243
|
+
const [created, setCreated] = useState<string>()
|
|
10244
|
+
return (
|
|
10245
|
+
<Form
|
|
10246
|
+
style={{width: 300}}
|
|
10247
|
+
onSubmit={async data => {
|
|
10248
|
+
const errors = await createPage(data)
|
|
10249
|
+
if (!errors) setCreated(String(data.get('title')))
|
|
10250
|
+
return errors
|
|
10251
|
+
}}
|
|
10252
|
+
>
|
|
10253
|
+
<Stack gap={16}>
|
|
10254
|
+
<TextField label="Title" name="title" required />
|
|
10255
|
+
<TextField label="Path" name="path" defaultValue="about" />
|
|
10256
|
+
<Select label="Type" name="type" defaultValue="page">
|
|
10257
|
+
<SelectItem value="page">Page</SelectItem>
|
|
10258
|
+
<SelectItem value="post">Blog post</SelectItem>
|
|
10259
|
+
</Select>
|
|
10260
|
+
<Button type="submit" color="primary">
|
|
10261
|
+
Create page
|
|
10262
|
+
</Button>
|
|
10263
|
+
{created && <Text size="sm">Created “{created}”</Text>}
|
|
10264
|
+
</Stack>
|
|
10265
|
+
</Form>
|
|
10266
|
+
)
|
|
10267
|
+
}
|
|
10268
|
+
```
|
|
10269
|
+
|
|
10270
|
+
## Validation
|
|
10271
|
+
|
|
10272
|
+
Native constraints are checked when the form is submitted: `required`, and `pattern`, `minLength` and `maxLength` on a TextField. `validationErrors` shows errors you already have, for example from a server, keyed by field name like the ones `onSubmit` returns. An `error` set on a field itself takes precedence.
|
|
10273
|
+
|
|
10274
|
+
The form controls of `alinea/components` take a `name` to be submitted under: text, number and select controls submit their value as a string, dates and times as ISO strings (`2026-09-23`, `14:30`) and MultipleSelect one entry per picked value. To submit a form from a dialog, pass `onSubmit` to [DialogContent](https://v2.alineacms.com/docs/components/dialog#submit-a-form): the dialog closes once it succeeds.
|
|
10275
|
+
|
|
10276
|
+
## Props
|
|
10277
|
+
|
|
10278
|
+
`Form`
|
|
10279
|
+
A form whose fields show validation errors: native constraints such as `required` once the form is submitted, and `validationErrors` or the errors returned by `onSubmit` by field name. An `error` set on a field itself takes precedence.
|
|
10280
|
+
| Prop | Type | Default | Description |
|
|
10281
|
+
| --- | --- | --- | --- |
|
|
10282
|
+
| validationErrors | `ValidationErrors` | | Errors keyed by field name, eg. from the server. A field shows its error until it is edited. |
|
|
10283
|
+
| onSubmit | `(data: FormData) => FormSubmitResult \| Promise<FormSubmitResult>` | | Called with the form data instead of submitting the form natively. Errors it returns are shown on the fields, next to `validationErrors`. |
|
|
10284
|
+
Also accepts `className` and `style`, `id` and `aria-*` labelling attributes, `data-*` attributes.
|
|
10285
|
+
|
|
10286
|
+
### Heading (https://v2.alineacms.com/docs/components/heading)
|
|
10287
|
+
A title in one of five sizes.
|
|
10288
|
+
|
|
10289
|
+
Example: Heading
|
|
10290
|
+
```tsx
|
|
10291
|
+
import {Heading} from 'alinea/components'
|
|
10292
|
+
|
|
9784
10293
|
export function HeadingExample() {
|
|
9785
10294
|
return (
|
|
9786
10295
|
<div style={{display: 'grid', gap: 8, width: 360}}>
|
|
@@ -9899,6 +10408,10 @@ export function LinkExample() {
|
|
|
9899
10408
|
}
|
|
9900
10409
|
```
|
|
9901
10410
|
|
|
10411
|
+
## Attributes
|
|
10412
|
+
|
|
10413
|
+
Besides `href`, the link passes `target`, `rel`, `download`, `title`, `tabIndex`, `lang`, `hrefLang`, `referrerPolicy` and `aria-current` to its `<a>`. Mark the link to the page that is shown with `aria-current="page"`.
|
|
10414
|
+
|
|
9902
10415
|
## Props
|
|
9903
10416
|
|
|
9904
10417
|
`Link`
|
|
@@ -9908,6 +10421,12 @@ export function LinkExample() {
|
|
|
9908
10421
|
| target | `string` | | |
|
|
9909
10422
|
| rel | `string` | | |
|
|
9910
10423
|
| download | `boolean \| string` | | |
|
|
10424
|
+
| title | `string` | | Tooltip text |
|
|
10425
|
+
| tabIndex | `number` | | |
|
|
10426
|
+
| lang | `string` | | |
|
|
10427
|
+
| hrefLang | `string` | | |
|
|
10428
|
+
| referrerPolicy | `HTMLAttributeReferrerPolicy` | | |
|
|
10429
|
+
| aria-current | `boolean \| 'page' \| 'step' \| 'location' \| 'date' \| 'time'` | | |
|
|
9911
10430
|
| variant | `'plain' \| 'underline'` | `'plain'` | `plain` underlines on hover, `underline` is always underlined |
|
|
9912
10431
|
| disabled | `boolean` | | |
|
|
9913
10432
|
| onClick | `(event: MouseEvent<HTMLAnchorElement>) => void` | | |
|
|
@@ -10084,35 +10603,27 @@ export function MultipleSelectExample() {
|
|
|
10084
10603
|
}
|
|
10085
10604
|
```
|
|
10086
10605
|
|
|
10606
|
+
## Values
|
|
10607
|
+
|
|
10608
|
+
The value is an array of the `value`s of the picked items, strings or numbers. With a `name` the field submits one form entry per value, like a native multiple select: read them with `formData.getAll(name)`.
|
|
10609
|
+
|
|
10087
10610
|
## Props
|
|
10088
10611
|
|
|
10089
10612
|
`MultipleSelect`
|
|
10090
10613
|
A labelled field to pick several values from a searchable list, the selected values are shown as removable tags.
|
|
10091
10614
|
| Prop | Type | Default | Description |
|
|
10092
10615
|
| --- | --- | --- | --- |
|
|
10093
|
-
| value | `Array<
|
|
10094
|
-
| defaultValue | `Array<
|
|
10095
|
-
| onValueChange | `(value: Array<
|
|
10616
|
+
| value | `Array<V>` | | The values of the selected MultipleSelectItems |
|
|
10617
|
+
| defaultValue | `Array<V>` | | |
|
|
10618
|
+
| onValueChange | `(value: Array<V>) => void` | | |
|
|
10096
10619
|
| placeholder | `string` | | |
|
|
10097
10620
|
| emptyMessage | `ReactNode` | `'No options'` | Shown in the list when no item matches the search, defaults to "No options" |
|
|
10098
|
-
| name | `string` | | Name of the hidden form input carrying the values |
|
|
10099
|
-
| label | `ReactNode` | | |
|
|
10100
|
-
| description | `ReactNode` | | |
|
|
10101
|
-
| error | `ReactNode` | | |
|
|
10102
|
-
| required | `boolean` | | |
|
|
10103
|
-
| disabled | `boolean` | | |
|
|
10104
|
-
| readOnly | `boolean` | | |
|
|
10105
|
-
| icon | `IconType \| ReactElement` | | |
|
|
10106
|
-
| shared | `boolean` | | Marks the field as shared between translations |
|
|
10107
|
-
| open | `boolean` | | |
|
|
10108
|
-
| defaultOpen | `boolean` | | |
|
|
10109
|
-
| onOpenChange | `(open: boolean) => void` | | |
|
|
10110
|
-
Also accepts `className` and `style`, `id` and `aria-*` labelling attributes, `data-*` attributes.
|
|
10621
|
+
| name | `string` | | Name of the hidden form input carrying the values, it submits one entry per selected value like a native multiple select: read them with `formData.getAll(name)` |
|
|
10111
10622
|
|
|
10112
10623
|
`MultipleSelectItem`
|
|
10113
10624
|
| Prop | Type | Default | Description |
|
|
10114
10625
|
| --- | --- | --- | --- |
|
|
10115
|
-
| value (required) | `
|
|
10626
|
+
| value (required) | `Key` | | |
|
|
10116
10627
|
| textValue | `string` | | Text used for searching and the tag, defaults to string children |
|
|
10117
10628
|
| disabled | `boolean` | | |
|
|
10118
10629
|
Also accepts `className` and `style`.
|
|
@@ -10216,6 +10727,10 @@ export function NumberFieldExample() {
|
|
|
10216
10727
|
}
|
|
10217
10728
|
```
|
|
10218
10729
|
|
|
10730
|
+
## Formatting
|
|
10731
|
+
|
|
10732
|
+
`formatOptions` takes the options of `Intl.NumberFormat`, such as a currency or a unit, and the number is shown and parsed in the locale of the dashboard. Digits are not grouped by default, since number fields often hold years or ids: pass `formatOptions={{useGrouping: true}}` to show `1,250` rather than `1250`. The value is a number, or `null` when the field is empty.
|
|
10733
|
+
|
|
10219
10734
|
## Props
|
|
10220
10735
|
|
|
10221
10736
|
`NumberField`
|
|
@@ -10227,7 +10742,7 @@ export function NumberFieldExample() {
|
|
|
10227
10742
|
| min | `number` | | |
|
|
10228
10743
|
| max | `number` | | |
|
|
10229
10744
|
| step | `number` | | |
|
|
10230
|
-
| formatOptions | `Intl.NumberFormatOptions` | |
|
|
10745
|
+
| formatOptions | `Intl.NumberFormatOptions` | | How the number is shown and parsed. Digits are not grouped unless `useGrouping` is set: number fields often hold years and ids |
|
|
10231
10746
|
| placeholder | `string` | | |
|
|
10232
10747
|
| steppers | `boolean` | `true` | Show increment and decrement buttons, defaults to true |
|
|
10233
10748
|
| name | `string` | | |
|
|
@@ -10641,7 +11156,7 @@ A text input for search queries with a clear button. The `icon` is shown inside
|
|
|
10641
11156
|
Also accepts `className` and `style`, `id` and `aria-*` labelling attributes, `data-*` attributes.
|
|
10642
11157
|
|
|
10643
11158
|
### Select (https://v2.alineacms.com/docs/components/select)
|
|
10644
|
-
Picks a single value from a list of options. The value is the `value` of the chosen SelectItem, and
|
|
11159
|
+
Picks a single value from a list of options. The value is the `value` of the chosen SelectItem, and `onValueChange` receives it, typed like the `value` you pass: values are strings or numbers. Set `clearable` to show a button that clears the value again, `onValueChange` then receives `null`.
|
|
10645
11160
|
|
|
10646
11161
|
Example: Select
|
|
10647
11162
|
```tsx
|
|
@@ -10704,7 +11219,7 @@ export function SelectGroupsExample() {
|
|
|
10704
11219
|
|
|
10705
11220
|
## States
|
|
10706
11221
|
|
|
10707
|
-
A placeholder shows while nothing is selected. Like the other form controls, a select takes `description`, `error`, `required`, `disabled` and `readOnly`.
|
|
11222
|
+
A placeholder shows while nothing is selected, and `clearable` adds a button to clear the selection. Like the other form controls, a select takes `description`, `error`, `required`, `disabled` and `readOnly`.
|
|
10708
11223
|
|
|
10709
11224
|
Example: Select: states
|
|
10710
11225
|
```tsx
|
|
@@ -10713,7 +11228,7 @@ import {Select, SelectItem} from 'alinea/components'
|
|
|
10713
11228
|
export function SelectStatesExample() {
|
|
10714
11229
|
return (
|
|
10715
11230
|
<div style={{display: 'grid', gap: 16, width: 280}}>
|
|
10716
|
-
<Select label="Category" placeholder="Choose a category">
|
|
11231
|
+
<Select label="Category" placeholder="Choose a category" clearable>
|
|
10717
11232
|
<SelectItem value="bedroom">Bedroom</SelectItem>
|
|
10718
11233
|
<SelectItem value="dining">Dining</SelectItem>
|
|
10719
11234
|
</Select>
|
|
@@ -10733,33 +11248,22 @@ export function SelectStatesExample() {
|
|
|
10733
11248
|
## Props
|
|
10734
11249
|
|
|
10735
11250
|
`Select`
|
|
10736
|
-
A labelled field to pick a single value from a list.
|
|
11251
|
+
A labelled field to pick a single value from a list. Set `clearable` to let the value be cleared again.
|
|
10737
11252
|
| Prop | Type | Default | Description |
|
|
10738
11253
|
| --- | --- | --- | --- |
|
|
10739
|
-
| value | `
|
|
10740
|
-
| defaultValue | `
|
|
10741
|
-
| onValueChange | `(value:
|
|
11254
|
+
| value | `V \| null` | | The value of the selected SelectItem |
|
|
11255
|
+
| defaultValue | `V \| null` | | |
|
|
11256
|
+
| onValueChange | `(value: V \| null) => void` | | Called with the selected value, or null when the value is cleared |
|
|
10742
11257
|
| placeholder | `string` | | |
|
|
11258
|
+
| clearable | `boolean` | | Show a button that clears the value |
|
|
10743
11259
|
| name | `string` | | Name of the hidden form input carrying the value |
|
|
10744
11260
|
| autoFocus | `boolean` | | |
|
|
10745
11261
|
| size | `'default' \| 'sm'` | `'default'` | `sm` is as tall as a default Button, defaults to `default` |
|
|
10746
|
-
| label | `ReactNode` | | |
|
|
10747
|
-
| description | `ReactNode` | | |
|
|
10748
|
-
| error | `ReactNode` | | |
|
|
10749
|
-
| required | `boolean` | | |
|
|
10750
|
-
| disabled | `boolean` | | |
|
|
10751
|
-
| readOnly | `boolean` | | |
|
|
10752
|
-
| icon | `IconType \| ReactElement` | | |
|
|
10753
|
-
| shared | `boolean` | | Marks the field as shared between translations |
|
|
10754
|
-
| open | `boolean` | | |
|
|
10755
|
-
| defaultOpen | `boolean` | | |
|
|
10756
|
-
| onOpenChange | `(open: boolean) => void` | | |
|
|
10757
|
-
Also accepts `className` and `style`, `id` and `aria-*` labelling attributes, `data-*` attributes.
|
|
10758
11262
|
|
|
10759
11263
|
`SelectItem`
|
|
10760
11264
|
| Prop | Type | Default | Description |
|
|
10761
11265
|
| --- | --- | --- | --- |
|
|
10762
|
-
| value (required) | `
|
|
11266
|
+
| value (required) | `Key` | | |
|
|
10763
11267
|
| disabled | `boolean` | | |
|
|
10764
11268
|
| icon | `IconType \| ReactElement` | | |
|
|
10765
11269
|
| textValue | `string` | | Text used for typeahead and the value, defaults to string children |
|
|
@@ -11021,6 +11525,48 @@ A circular progress indicator
|
|
|
11021
11525
|
| value | `number` | | Progress from 0 to 100, leave out for an indeterminate spinner |
|
|
11022
11526
|
Also accepts `className` and `style`, `id` and `aria-*` labelling attributes, `data-*` attributes.
|
|
11023
11527
|
|
|
11528
|
+
### Stack (https://v2.alineacms.com/docs/components/stack)
|
|
11529
|
+
Lays out its children in a column, or a row with `direction="row"`, with a `gap` between them: a number is in pixels, a string any CSS length. `align` positions the items across the stack (they stretch by default), `justify` along it, and `wrap` lets a row continue on the next line.
|
|
11530
|
+
|
|
11531
|
+
Example: Stack
|
|
11532
|
+
```tsx
|
|
11533
|
+
import {Badge, Button, Stack, TextField} from 'alinea/components'
|
|
11534
|
+
|
|
11535
|
+
export function StackExample() {
|
|
11536
|
+
return (
|
|
11537
|
+
<Stack gap={16} style={{width: 320}}>
|
|
11538
|
+
<Stack direction="row" align="center" justify="between">
|
|
11539
|
+
<strong>About us</strong>
|
|
11540
|
+
<Badge status="draft">Draft</Badge>
|
|
11541
|
+
</Stack>
|
|
11542
|
+
<TextField label="Title" defaultValue="About us" />
|
|
11543
|
+
<TextField label="Path" defaultValue="about" />
|
|
11544
|
+
<Stack direction="row" gap={8} justify="end" wrap>
|
|
11545
|
+
<Button variant="outline">Cancel</Button>
|
|
11546
|
+
<Button color="primary">Save</Button>
|
|
11547
|
+
</Stack>
|
|
11548
|
+
</Stack>
|
|
11549
|
+
)
|
|
11550
|
+
}
|
|
11551
|
+
```
|
|
11552
|
+
|
|
11553
|
+
With `asChild` the stack lays out its single child element instead of rendering a div. `Stack` replaces the deprecated `HStack` and `VStack` of `alinea/ui`: `<Stack direction="row">` is an HStack.
|
|
11554
|
+
|
|
11555
|
+
## Props
|
|
11556
|
+
|
|
11557
|
+
`Stack`
|
|
11558
|
+
Lays out its children in a row or column with a gap between them
|
|
11559
|
+
| Prop | Type | Default | Description |
|
|
11560
|
+
| --- | --- | --- | --- |
|
|
11561
|
+
| direction | `'row' \| 'column'` | `'column'` | Defaults to `column` |
|
|
11562
|
+
| gap | `number \| string` | | Space between the items, a number is in pixels |
|
|
11563
|
+
| align | `'start' \| 'center' \| 'end' \| 'stretch' \| 'baseline'` | | Cross axis alignment of the items, they stretch by default |
|
|
11564
|
+
| justify | `'start' \| 'center' \| 'end' \| 'between'` | | Main axis distribution of the items |
|
|
11565
|
+
| wrap | `boolean` | | Wrap items onto multiple lines |
|
|
11566
|
+
| asChild | `boolean` | | Lay out the single child element instead of rendering a div |
|
|
11567
|
+
| ref | `Ref<HTMLDivElement>` | | |
|
|
11568
|
+
Also accepts `className` and `style`, `id` and `aria-*` labelling attributes, `data-*` attributes.
|
|
11569
|
+
|
|
11024
11570
|
### Surface (https://v2.alineacms.com/docs/components/surface)
|
|
11025
11571
|
A bordered panel for a group of content.
|
|
11026
11572
|
|
|
@@ -11240,6 +11786,84 @@ export function TableSelectionExample() {
|
|
|
11240
11786
|
}
|
|
11241
11787
|
```
|
|
11242
11788
|
|
|
11789
|
+
## Load more rows
|
|
11790
|
+
|
|
11791
|
+
`onLoadMore` is called when the rows are scrolled close to the end, to fetch the next page. While `loading` is set, a spinner shows below the rows, or in place of them while there are none. `useAsyncList` keeps the loaded pages: its `load` function receives the `cursor` the previous page returned, and `loadMore` does nothing once a page returns no cursor. Its `items`, `loading`, `loadMore`, `sortDescriptor` and `sort`, and `selectedKeys` and `setSelectedKeys` plug into the props of the table. `update(key, item)`, `remove(...keys)`, `prepend(...items)` and `append(...items)` change the loaded items without loading them again, for example after a dialog saves a row.
|
|
11792
|
+
|
|
11793
|
+
Example: Table: async
|
|
11794
|
+
```tsx
|
|
11795
|
+
import {
|
|
11796
|
+
Table,
|
|
11797
|
+
TableCell,
|
|
11798
|
+
TableRow,
|
|
11799
|
+
TableTitle,
|
|
11800
|
+
useAsyncList
|
|
11801
|
+
} from 'alinea/components'
|
|
11802
|
+
|
|
11803
|
+
interface Product {
|
|
11804
|
+
id: string
|
|
11805
|
+
title: string
|
|
11806
|
+
price: number
|
|
11807
|
+
}
|
|
11808
|
+
|
|
11809
|
+
const materials = ['Linen', 'Oak', 'Wool', 'Walnut', 'Ceramic']
|
|
11810
|
+
const kinds = ['shirt', 'chair', 'throw', 'stool', 'lamp', 'table']
|
|
11811
|
+
|
|
11812
|
+
// Stands in for a paged API: 20 products per page, 100 in total
|
|
11813
|
+
async function fetchProducts(page: number) {
|
|
11814
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
11815
|
+
const items: Array<Product> = Array.from({length: 20}, (_, i) => {
|
|
11816
|
+
const n = page * 20 + i
|
|
11817
|
+
const title = `${materials[n % 5]} ${kinds[n % 6]} ${n + 1}`
|
|
11818
|
+
return {id: `product-${n}`, title, price: 40 + ((n * 37) % 300)}
|
|
11819
|
+
})
|
|
11820
|
+
return {items, cursor: page < 4 ? page + 1 : undefined}
|
|
11821
|
+
}
|
|
11822
|
+
|
|
11823
|
+
export function TableAsyncExample() {
|
|
11824
|
+
const list = useAsyncList<Product, number>({
|
|
11825
|
+
load: ({cursor = 0}) => fetchProducts(cursor)
|
|
11826
|
+
})
|
|
11827
|
+
return (
|
|
11828
|
+
<div style={{width: 420, height: 260}}>
|
|
11829
|
+
<Table
|
|
11830
|
+
aria-label="Products"
|
|
11831
|
+
items={list.items}
|
|
11832
|
+
columns={[
|
|
11833
|
+
{id: 'title', header: 'Title', width: '1fr'},
|
|
11834
|
+
{id: 'price', header: 'Price', width: 100, align: 'end'}
|
|
11835
|
+
]}
|
|
11836
|
+
loading={list.loading}
|
|
11837
|
+
onLoadMore={list.loadMore}
|
|
11838
|
+
>
|
|
11839
|
+
{product => (
|
|
11840
|
+
<TableRow id={product.id} textValue={product.title}>
|
|
11841
|
+
<TableTitle title={product.title} />
|
|
11842
|
+
<TableCell align="end">€{product.price}</TableCell>
|
|
11843
|
+
</TableRow>
|
|
11844
|
+
)}
|
|
11845
|
+
</Table>
|
|
11846
|
+
</div>
|
|
11847
|
+
)
|
|
11848
|
+
}
|
|
11849
|
+
```
|
|
11850
|
+
|
|
11851
|
+
```tsx
|
|
11852
|
+
import {useAsyncList} from 'alinea/components'
|
|
11853
|
+
|
|
11854
|
+
const list = useAsyncList({
|
|
11855
|
+
async load({cursor, filterText, sort, signal}) {
|
|
11856
|
+
const res = await fetch(`/api/products?page=${cursor ?? 1}`, {signal})
|
|
11857
|
+
const {items, next} = await res.json()
|
|
11858
|
+
// Leave out the cursor, or return null, on the last page
|
|
11859
|
+
return {items, cursor: next}
|
|
11860
|
+
}
|
|
11861
|
+
})
|
|
11862
|
+
// list.items, list.loading, list.hasMore, list.error,
|
|
11863
|
+
// list.loadMore(), list.reload(), list.setFilterText(text), list.sort(sort),
|
|
11864
|
+
// list.update(id, item), list.remove(id), list.prepend(item)
|
|
11865
|
+
```
|
|
11866
|
+
|
|
11243
11867
|
## Props
|
|
11244
11868
|
|
|
11245
11869
|
`Table`
|
|
@@ -11260,6 +11884,8 @@ export function TableSelectionExample() {
|
|
|
11260
11884
|
| onSortChange | `(descriptor: SortDescriptor) => void` | | |
|
|
11261
11885
|
| onRowAction | `(key: Key) => void` | | Called when a row is activated (see `selectionBehavior`), rows can override it with their own `onAction` |
|
|
11262
11886
|
| renderEmptyState | `() => ReactNode` | | |
|
|
11887
|
+
| loading | `boolean` | | Shows a spinner in place of the rows while there are none, below the rows otherwise |
|
|
11888
|
+
| onLoadMore | `() => void` | | Called when the rows are scrolled close to the end, to load the next page (see useAsyncList) |
|
|
11263
11889
|
| dependencies | `ReadonlyArray<unknown>` | | Rows are cached per item, list the values `children` reads besides the item to render them again when those change |
|
|
11264
11890
|
| children (required) | `(item: T) => ReactElement` | | |
|
|
11265
11891
|
|
|
@@ -11302,6 +11928,9 @@ Also accepts `className` and `style`.
|
|
|
11302
11928
|
| suffix | `ReactNode` | | Shown right after the title, eg. a status icon |
|
|
11303
11929
|
Also accepts `className` and `style`.
|
|
11304
11930
|
|
|
11931
|
+
`useAsyncList`
|
|
11932
|
+
Loads a list of items, with sorting, filtering and paging
|
|
11933
|
+
|
|
11305
11934
|
## Accessibility
|
|
11306
11935
|
|
|
11307
11936
|
Built on the React Aria grid: the arrow keys move between rows, Space selects a row and Enter runs its action. Sortable column headers announce their sort direction.
|
|
@@ -11570,7 +12199,7 @@ export function TextFieldStatesExample() {
|
|
|
11570
12199
|
|
|
11571
12200
|
## Multiline
|
|
11572
12201
|
|
|
11573
|
-
A multiline field starts at `rows` lines and grows as editors type.
|
|
12202
|
+
A multiline field starts at `rows` lines and grows as editors type, up to `maxRows` lines when you set it, then it scrolls.
|
|
11574
12203
|
|
|
11575
12204
|
Example: TextField: multiline
|
|
11576
12205
|
```tsx
|
|
@@ -11582,6 +12211,7 @@ export function TextFieldMultilineExample() {
|
|
|
11582
12211
|
label="Description"
|
|
11583
12212
|
multiline
|
|
11584
12213
|
rows={4}
|
|
12214
|
+
maxRows={8}
|
|
11585
12215
|
defaultValue="A relaxed shirt in washed linen that softens with every wear."
|
|
11586
12216
|
style={{width: 300}}
|
|
11587
12217
|
/>
|
|
@@ -11622,6 +12252,10 @@ export function TextFieldIconsExample() {
|
|
|
11622
12252
|
}
|
|
11623
12253
|
```
|
|
11624
12254
|
|
|
12255
|
+
## Native input attributes
|
|
12256
|
+
|
|
12257
|
+
`type`, `name`, `autoComplete`, `inputMode`, `pattern`, `minLength` and `maxLength` are passed to the input. `inputMode` picks the virtual keyboard on touch devices, eg. `numeric` for a code, and `pattern` is a regular expression the value must match, checked when the field is in a [Form](https://v2.alineacms.com/docs/components/form).
|
|
12258
|
+
|
|
11625
12259
|
## Props
|
|
11626
12260
|
|
|
11627
12261
|
`TextField`
|
|
@@ -11634,6 +12268,7 @@ export function TextFieldIconsExample() {
|
|
|
11634
12268
|
| type | `'text' \| 'email' \| 'url' \| 'password' \| 'tel' \| 'search'` | `'text'` | |
|
|
11635
12269
|
| multiline | `boolean` | | Render a textarea that grows with its content |
|
|
11636
12270
|
| rows | `number` | `1` | Minimum number of rows of a multiline field |
|
|
12271
|
+
| maxRows | `number` | | Maximum number of rows a multiline field grows to before it scrolls |
|
|
11637
12272
|
| startIcon | `IconType \| ReactElement` | | Icon displayed inside the input, before the text |
|
|
11638
12273
|
| endIcon | `IconType \| ReactElement` | | Icon displayed inside the input, after the text |
|
|
11639
12274
|
| name | `string` | | |
|
|
@@ -11641,6 +12276,8 @@ export function TextFieldIconsExample() {
|
|
|
11641
12276
|
| autoComplete | `string` | | |
|
|
11642
12277
|
| maxLength | `number` | | |
|
|
11643
12278
|
| minLength | `number` | | |
|
|
12279
|
+
| inputMode | `'none' \| 'text' \| 'tel' \| 'url' \| 'email' \| 'numeric' \| 'decimal' \| 'search'` | | Which virtual keyboard to show, eg. `numeric` for a code |
|
|
12280
|
+
| pattern | `string` | | A regular expression the value must match to be valid |
|
|
11644
12281
|
| onBlur | `(event: FocusEvent<TextFieldElement>) => void` | | |
|
|
11645
12282
|
| onFocus | `(event: FocusEvent<TextFieldElement>) => void` | | |
|
|
11646
12283
|
| onKeyDown | `(event: KeyboardEvent<TextFieldElement>) => void` | | |
|
|
@@ -11679,13 +12316,17 @@ export function TimeFieldExample() {
|
|
|
11679
12316
|
}
|
|
11680
12317
|
```
|
|
11681
12318
|
|
|
12319
|
+
## Value
|
|
12320
|
+
|
|
12321
|
+
The value is an ISO time string: `14:30`, or `14:30:15` with `granularity="second"`. With a `name` the field submits the same string in a form, like a native time input, and an empty string when no time is set. `hourCycle` only changes how the time is shown.
|
|
12322
|
+
|
|
11682
12323
|
## Props
|
|
11683
12324
|
|
|
11684
12325
|
`TimeField`
|
|
11685
12326
|
| Prop | Type | Default | Description |
|
|
11686
12327
|
| --- | --- | --- | --- |
|
|
11687
12328
|
| locale | `string; /** The time, `HH:mm` or `HH:mm:ss` */ value?: string \| null; defaultValue?: string \| null; /** Receives `HH:mm`, or `HH:mm:ss` when granularity is `second` */ onValueChange?: (value: string \| null) => void; /** The earliest valid time, `HH:mm` or `HH:mm:ss` */ min?: string; /** The latest valid time, `HH:mm` or `HH:mm:ss` */ max?: string; /** The smallest unit that can be edited, defaults to `minute` */ granularity?: 'minute' \| 'second'; /** Defaults to the locale's hour cycle */ hourCycle?: 12 \| 24` | | BCP 47 locale used to format dates, eg. `en-GB`, defaults to the user's locale |
|
|
11688
|
-
| name | `string` | |
|
|
12329
|
+
| name | `string` | | Name of the hidden form input, which submits the time like a native time input: `HH:mm`, or `HH:mm:ss` when granularity is `second`, and an empty string when no time is set |
|
|
11689
12330
|
| autoFocus | `boolean` | | |
|
|
11690
12331
|
| label | `ReactNode` | | |
|
|
11691
12332
|
| description | `ReactNode` | | |
|
|
@@ -12095,7 +12736,28 @@ export function range(
|
|
|
12095
12736
|
}
|
|
12096
12737
|
```
|
|
12097
12738
|
|
|
12098
|
-
`Field.Create<Value, Options>` is the type of a field that stores `Value` (anything JSON can hold) and accepts your `Options` next to the standard ones: `initialValue`, `required`, `readOnly`, `hidden`, `shared` and `
|
|
12739
|
+
`Field.Create<Value, Options>` is the type of a field that stores `Value` (anything JSON can hold) and accepts your `Options` next to the standard ones: `initialValue`, `required`, `readOnly`, `hidden`, `shared`, `validate` and `width`, the width of the field in the entry form. Queries return the stored value as it is.
|
|
12740
|
+
|
|
12741
|
+
To describe the options as an interface, extend `FieldOptions` from `alinea/core` and type the options argument with `WithoutLabel`:
|
|
12742
|
+
|
|
12743
|
+
File: fields/Rating.ts
|
|
12744
|
+
```tsx
|
|
12745
|
+
import {Field} from 'alinea'
|
|
12746
|
+
import type {FieldOptions, WithoutLabel} from 'alinea'
|
|
12747
|
+
|
|
12748
|
+
export interface RatingOptions extends FieldOptions<number> {
|
|
12749
|
+
max?: number
|
|
12750
|
+
}
|
|
12751
|
+
|
|
12752
|
+
export type RatingField = Field.Create<number, RatingOptions>
|
|
12753
|
+
|
|
12754
|
+
export function rating(
|
|
12755
|
+
label: string,
|
|
12756
|
+
options: WithoutLabel<RatingOptions> = {}
|
|
12757
|
+
): RatingField {
|
|
12758
|
+
return Field.create({label, options, view: '@/fields/Rating.view'})
|
|
12759
|
+
}
|
|
12760
|
+
```
|
|
12099
12761
|
|
|
12100
12762
|
`Field.create` takes:
|
|
12101
12763
|
|
|
@@ -12221,9 +12883,13 @@ export function priority(
|
|
|
12221
12883
|
}
|
|
12222
12884
|
```
|
|
12223
12885
|
|
|
12886
|
+
The entry editor sets `--alinea-header-height` to the height of its header. A view that fills the visible editor can size itself with it, for example `height: calc(100dvh - var(--alinea-header-height))`.
|
|
12887
|
+
|
|
12888
|
+
Style your own markup with the [theme tokens](https://v2.alineacms.com/docs/components#theme-tokens), such as `--alinea-fg-muted`, `--alinea-border` and `--alinea-radius`, so it follows light and dark mode. `--alinea-workspace-color` holds the `color` of the current workspace.
|
|
12889
|
+
|
|
12224
12890
|
## The alinea/cms API
|
|
12225
12891
|
|
|
12226
|
-
Everything in `alinea/cms` works in field views and in the other components the dashboard renders: `Field.view` sections and
|
|
12892
|
+
Everything in `alinea/cms` works in field views and in the other components the dashboard renders for an entry: `Field.view` sections, entry `actions` and a type's `view`. A root's `view` is not part of an entry: the hooks that read the dashboard, such as `useGraph`, `useNavigate`, `useLocale` and `useUser`, and `EntryTable` work there, the field hooks, `EditField` and `EditFields` don't, and `useEntry()` returns `null`.
|
|
12227
12893
|
|
|
12228
12894
|
Field hooks, for the field passed to a view or another field of the same entry, list row or object:
|
|
12229
12895
|
|
|
@@ -12232,24 +12898,55 @@ Field hooks, for the field passed to a view or another field of the same entry,
|
|
|
12232
12898
|
- `useFieldOptions(field)`: the field's options, including `label` and the resolved `readOnly` and `hidden`.
|
|
12233
12899
|
- `useFieldError(field)`: the validation message from `required` or `validate`, if any.
|
|
12234
12900
|
- `useFieldKey(field)`: the key the field is stored under, such as `'title'`.
|
|
12235
|
-
-
|
|
12901
|
+
- Other fields: pass any field of the same list row or object, of a parent row or of the entry to these hooks, it resolves in the nearest scope that holds it. Import the type from your schema (`useFieldValue(BlogPost.title)`) or pass the field through your field's options. A view shared by types that each define their own field passes its key instead: `useFieldValue('path')` reads that field of the same row, untyped. `useSiblingFieldValue(key)` is deprecated.
|
|
12902
|
+
|
|
12903
|
+
A field in the rows of a list can read a field of the entry, here to suggest the post title as a caption:
|
|
12904
|
+
|
|
12905
|
+
File: fields/Caption.view.tsx
|
|
12906
|
+
```tsx
|
|
12907
|
+
import {
|
|
12908
|
+
type FieldViewProps,
|
|
12909
|
+
useField,
|
|
12910
|
+
useFieldOptions,
|
|
12911
|
+
useFieldValue
|
|
12912
|
+
} from 'alinea/cms'
|
|
12913
|
+
import {TextField} from 'alinea/components'
|
|
12914
|
+
import {BlogPost} from '@/schema'
|
|
12915
|
+
import type {CaptionField} from './Caption'
|
|
12916
|
+
|
|
12917
|
+
export function CaptionView({field}: FieldViewProps<CaptionField>) {
|
|
12918
|
+
const [value, setValue] = useField(field)
|
|
12919
|
+
const {label} = useFieldOptions(field)
|
|
12920
|
+
// A field of the entry, read from inside a list row
|
|
12921
|
+
const title = useFieldValue(BlogPost.title)
|
|
12922
|
+
return (
|
|
12923
|
+
<TextField
|
|
12924
|
+
label={label}
|
|
12925
|
+
value={value}
|
|
12926
|
+
placeholder={title}
|
|
12927
|
+
onValueChange={setValue}
|
|
12928
|
+
/>
|
|
12929
|
+
)
|
|
12930
|
+
}
|
|
12931
|
+
```
|
|
12236
12932
|
|
|
12237
12933
|
Entry and dashboard hooks:
|
|
12238
12934
|
|
|
12239
|
-
- `useEntry()`: the entry being edited, with its `id`, `
|
|
12935
|
+
- `useEntry()`: the entry being edited, with its `id`, `type`, `title`, `path`, `url`, `locale`, `status`, `workspace`, `root`, `parentId` and `data`. `data` holds the field values as saved, read the live value of a field while it is edited with `useFieldValue(field)`. `null` outside an entry.
|
|
12240
12936
|
- `useLocale()`: the locale being edited, or `null` for content without translations.
|
|
12241
12937
|
- `useGraph()`: query content from the dashboard with the same API as `cms`, for example to list entries in a picker.
|
|
12242
12938
|
- `useUser()`, `usePolicy()`: the signed-in user and their [permissions](https://v2.alineacms.com/docs/roles-permissions).
|
|
12243
|
-
- `useNavigate()`: returns a function that opens an entry or root in the dashboard: `navigate({
|
|
12939
|
+
- `useNavigate()`: returns a function that opens an entry or root in the dashboard: `navigate({entryId, locale})` looks up the entry's workspace and root, `navigate({workspace, root})` opens a root. Pass `view: 'edit'` or `view: 'overview'` to pick the form or the list of children. The editor is asked to confirm first when there are unsaved changes.
|
|
12940
|
+
- `href(location)` from `alinea`: the href of the same locations, for a link that also opens in a new tab: `<a href={href({entryId: post.id})}>`. It is a plain function, so it also works in table rows, in the `help` of a field and on the server, where you prefix the dashboard's url: `${baseUrl}/admin${href({entryId})}`.
|
|
12244
12941
|
- `usePreviewMetadata()`: the title, description and other metadata the live preview reports for the current page.
|
|
12245
12942
|
|
|
12246
12943
|
Components and types:
|
|
12247
12944
|
|
|
12248
12945
|
- `FieldChrome`: the label, help text and error of a field around your own control.
|
|
12249
|
-
- `EditField`, `EditFields`: render a field, or a record of fields, with the view it is configured with, for example to lay out existing fields in a `Field.view` section.
|
|
12946
|
+
- `EditField`, `EditFields`: render a field, or a record of fields, with the view it is configured with, for example to lay out existing fields in a `Field.view` section. Pass an object field to `EditFields` to render its fields, or some of them, in the scope of the object: `<EditFields field={Product.dimensions} fields={{width: Product.dimensions.width}} />`.
|
|
12250
12947
|
- `EntryTable`: a table of entries with the columns of an [overview](https://v2.alineacms.com/docs/overviews#entry-tables-in-your-own-views), for example to list related entries in a `Field.view` section.
|
|
12251
12948
|
- `FieldViewProps<F>`, `SectionViewProps`, `TypeViewProps`, `RootViewProps`: the props the dashboard passes to a field view, a `Field.view` component, a type's `view` and a root's `view`.
|
|
12252
|
-
- `OverviewCellProps`, `OverviewActionProps`, `OverviewEntry`: the props of an overview column's `view` and of an overview action, and the entry they receive.
|
|
12949
|
+
- `OverviewCellProps`, `OverviewActionProps`, `OverviewEntry`: the props of an overview column's `view` and of an overview action, and the entry they receive. `EntryActionProps` and `DashboardEntry`: the props of an entry action and the entry `useEntry()` returns.
|
|
12253
12950
|
|
|
12254
12951
|
## Show a component between fields
|
|
12255
12952
|
|
|
@@ -12285,6 +12982,77 @@ export function ProductUrls() {
|
|
|
12285
12982
|
}
|
|
12286
12983
|
```
|
|
12287
12984
|
|
|
12985
|
+
## Replace the form of a type
|
|
12986
|
+
|
|
12987
|
+
A type's `view` replaces the form of its entries with your own component, for example to edit the fields in a layout of your own. The entry header stays above it, with the title, the status, the publish and draft buttons, the type's `actions` and the sidebar with history and references. The view fills the rest of the editor, without padding.
|
|
12988
|
+
|
|
12989
|
+
File: schema/Product.ts
|
|
12990
|
+
```tsx
|
|
12991
|
+
import {Config, Field} from 'alinea'
|
|
12992
|
+
|
|
12993
|
+
export const Product = Config.document('Product', {
|
|
12994
|
+
view: '@/views/ProductView.view#ProductView',
|
|
12995
|
+
fields: {
|
|
12996
|
+
summary: Field.text('Summary', {multiline: true}),
|
|
12997
|
+
price: Field.number('Price')
|
|
12998
|
+
}
|
|
12999
|
+
})
|
|
13000
|
+
```
|
|
13001
|
+
|
|
13002
|
+
File: views/ProductView.view.tsx
|
|
13003
|
+
```tsx
|
|
13004
|
+
import {EditField, useField} from 'alinea/cms'
|
|
13005
|
+
import {TextField} from 'alinea/components'
|
|
13006
|
+
import {Product} from '@/schema/Product'
|
|
13007
|
+
|
|
13008
|
+
export function ProductView() {
|
|
13009
|
+
const [summary = '', setSummary] = useField(Product.summary)
|
|
13010
|
+
return (
|
|
13011
|
+
<div style={{display: 'grid', gap: 16, padding: 24}}>
|
|
13012
|
+
<TextField
|
|
13013
|
+
label="Summary"
|
|
13014
|
+
multiline
|
|
13015
|
+
value={summary}
|
|
13016
|
+
onValueChange={setSummary}
|
|
13017
|
+
/>
|
|
13018
|
+
<EditField field={Product.price} />
|
|
13019
|
+
</div>
|
|
13020
|
+
)
|
|
13021
|
+
}
|
|
13022
|
+
```
|
|
13023
|
+
|
|
13024
|
+
The view receives `TypeViewProps`: the `type` of the entry. It edits the entry like the form does: the field hooks, `EditField` and `EditFields` read and write the fields of the entry, the changes show up as unsaved edits in the header and are kept until the editor publishes or saves a draft, and leaving the entry with unsaved changes asks to confirm first. To fill the editor, size the view with `height: 100%`.
|
|
13025
|
+
|
|
13026
|
+
## Add actions to the entry header
|
|
13027
|
+
|
|
13028
|
+
Buttons that act on the entry, such as an export or a button that creates a related entry, go in the `actions` of its type. They render in the entry header next to the publish button and receive `EntryActionProps`: the `entry` and the `locale` it is edited in. The hooks of `alinea/cms` read and write its fields.
|
|
13029
|
+
|
|
13030
|
+
File: schema/Product.ts
|
|
13031
|
+
```tsx
|
|
13032
|
+
import {Config, Field} from 'alinea'
|
|
13033
|
+
|
|
13034
|
+
export const Product = Config.document('Product', {
|
|
13035
|
+
fields: {title: Field.text('Title')},
|
|
13036
|
+
actions: ['@/views/ExportProduct.view#ExportProduct']
|
|
13037
|
+
})
|
|
13038
|
+
```
|
|
13039
|
+
|
|
13040
|
+
File: views/ExportProduct.view.tsx
|
|
13041
|
+
```tsx
|
|
13042
|
+
import {type EntryActionProps, useFieldValue} from 'alinea/cms'
|
|
13043
|
+
import {Button} from 'alinea/components'
|
|
13044
|
+
import {Product} from '@/schema/Product'
|
|
13045
|
+
|
|
13046
|
+
export function ExportProduct({entry}: EntryActionProps) {
|
|
13047
|
+
const title = useFieldValue(Product.title)
|
|
13048
|
+
return (
|
|
13049
|
+
<Button onClick={() => window.open(`/api/export/${entry.id}`)}>
|
|
13050
|
+
Export {title}
|
|
13051
|
+
</Button>
|
|
13052
|
+
)
|
|
13053
|
+
}
|
|
13054
|
+
```
|
|
13055
|
+
|
|
12288
13056
|
## Upgrading from Alinea 1.x
|
|
12289
13057
|
|
|
12290
13058
|
Field views from 1.x keep working, with deprecated APIs:
|
|
@@ -12292,6 +13060,7 @@ Field views from 1.x keep working, with deprecated APIs:
|
|
|
12292
13060
|
- `useField` from `alinea/dashboard` returns `{value, mutator, options, label, error}`. Replace it with the tuple `useField` from `alinea/cms` and the separate `useFieldOptions` and `useFieldError`. `useFieldValue`, `useFieldOptions`, `useFieldError` and `useFieldKey` also move to `alinea/cms`, and `useFieldMutator` becomes `useFieldSetter`.
|
|
12293
13061
|
- `InputLabel` from `alinea/dashboard` is now the `Field` component. It no longer shows `help` from `{...options}`: use `FieldChrome` from `alinea/cms`, which reads it from the field, or pass the help text to `Field` from `alinea/components` as `description`.
|
|
12294
13062
|
- `useLocale`, `useGraph` and `useEntryEditor` from `alinea/dashboard/hook/*` become `useLocale`, `useGraph` and `useEntry` from `alinea/cms`.
|
|
13063
|
+
- `useEntryEditor` still works as a shim for views that only read the entry: `activeVersion` has the `id`, `locale`, `path`, `status`, `title` and `type` of the entry, and `activeVersion.data` only has `path`. Reading another field of it throws, read field values with `useFieldValue(field)` instead. `useEntry()` returns the same entry as a `DashboardEntry`, with its saved field values in `data`.
|
|
12295
13064
|
- Hooks imported from `alinea/dashboard/hooks` still work, but that module is internal: import them from `alinea/cms`.
|
|
12296
13065
|
- Components from `alinea/ui` were removed. Use [`alinea/components`](https://v2.alineacms.com/docs/components).
|
|
12297
13066
|
|
|
@@ -12362,16 +13131,18 @@ export const products = Config.root('Products', {
|
|
|
12362
13131
|
- `sort`: the default order of the children, in the overview and in the sidebar tree. See [Sorting](#sorting).
|
|
12363
13132
|
- `sorts`: the orders editors can pick in the filter and sort menu, keyed by name. See [Sort options](#sort-options).
|
|
12364
13133
|
- `filters`: filters editors can apply in the filter and sort menu, keyed by name. See [Filters](#filters).
|
|
13134
|
+
- `tabs`: tabs above the listed entries, keyed by a name kept in the url. See [Tabs](#tabs).
|
|
12365
13135
|
- `layout`: `'table'` or `'cards'`, the layout the overview opens in. Editors can still switch.
|
|
12366
13136
|
- `thumbnail`: the image shown on cards: an image field, or one per type (`{BlogPost: BlogPost.cover, Event: Event.poster}`). Defaults to the first image found in the fields of the entry.
|
|
12367
13137
|
- `actions`: components rendered in the toolbar of the overview, such as an export button. See [Actions](#actions).
|
|
13138
|
+
- `header`: a component rendered between the toolbar and the listed entries, such as totals. See [Header](#header).
|
|
12368
13139
|
|
|
12369
13140
|
## Columns
|
|
12370
13141
|
|
|
12371
13142
|
`Config.column(options)` takes:
|
|
12372
13143
|
|
|
12373
13144
|
- `header` (required): the column header.
|
|
12374
|
-
- `select`: what the column shows. Anything a [query](https://v2.alineacms.com/docs/query) can select: a field (`Product.price`), an expression (`Query.url`), a query of linked entries, or an object of those.
|
|
13145
|
+
- `select`: what the column shows. Anything a [query](https://v2.alineacms.com/docs/query) can select: a field (`Product.price`), an expression (`Query.url`), a query of linked entries, a relation such as `Query.referencedBy`, or an object of those.
|
|
12375
13146
|
- `format`: a function that turns the selected value into text: `(value, {locale}) => string`.
|
|
12376
13147
|
- `view`: a React component that renders the cell, see [Custom cells](#custom-cells).
|
|
12377
13148
|
- `sortBy`, `sortable`: how the column sorts, see [Sorting](#sorting).
|
|
@@ -12484,7 +13255,34 @@ Force them on or off with `builtins`, for example `{type: true}` to always show
|
|
|
12484
13255
|
|
|
12485
13256
|
## Sorting
|
|
12486
13257
|
|
|
12487
|
-
Editors sort an overview by clicking a column header, or from the filter and sort menu. The title, the built-in columns, expressions of the entry (such as `Query.title`) and fields that hold a single value (text, number, date, select, check, path, ...) are sortable. Columns that select links, lists, multiple selects, rich text or JSON need `sortBy`: an expression, or a query of a linked entry that selects a single expression, such as `Product.brand.first({select: Query.title})`. Set `sortable: false` to turn sorting off for a column.
|
|
13258
|
+
Editors sort an overview by clicking a column header, or from the filter and sort menu. The title, the built-in columns, expressions of the entry (such as `Query.title`) and fields that hold a single value (text, number, date, select, check, path, ...) are sortable, and so are columns that count related entries, such as `Query.referencedBy({type: Product, count: true})`. Columns that select links, lists, multiple selects, rich text or JSON need `sortBy`: an expression, or a query of a linked entry that selects a single expression, such as `Product.brand.first({select: Query.title})`. Set `sortable: false` to turn sorting off for a column.
|
|
13259
|
+
|
|
13260
|
+
A column that counts the products of each brand, which editors can sort by:
|
|
13261
|
+
|
|
13262
|
+
File: schema/Brands.ts
|
|
13263
|
+
```tsx
|
|
13264
|
+
import {Config, Query} from 'alinea'
|
|
13265
|
+
import {Product} from './Product'
|
|
13266
|
+
|
|
13267
|
+
export const Brands = Config.document('Brands', {
|
|
13268
|
+
contains: ['Brand'],
|
|
13269
|
+
fields: {},
|
|
13270
|
+
overview: {
|
|
13271
|
+
columns: {
|
|
13272
|
+
products: Config.column({
|
|
13273
|
+
header: 'Products',
|
|
13274
|
+
width: 100,
|
|
13275
|
+
align: 'end',
|
|
13276
|
+
select: Query.referencedBy({
|
|
13277
|
+
type: Product,
|
|
13278
|
+
field: Product.brand,
|
|
13279
|
+
count: true
|
|
13280
|
+
})
|
|
13281
|
+
})
|
|
13282
|
+
}
|
|
13283
|
+
}
|
|
13284
|
+
})
|
|
13285
|
+
```
|
|
12488
13286
|
|
|
12489
13287
|
Sorting runs in the database query, and entries without a value come last. It only changes what the editor sees, never the stored order of the entries. While a list is sorted, "Sorted by Price · Reset" shows in the toolbar and rows can't be reordered; Reset goes back to the default order. The sort is kept in the url (`?sort=price`, or `?sort=-price` for descending), so it survives a reload and can be shared, and it is restored when the editor comes back to the overview in the same session. Search results stay ordered by relevance.
|
|
12490
13288
|
|
|
@@ -12555,6 +13353,74 @@ export const Products = Config.document('Products', {
|
|
|
12555
13353
|
})
|
|
12556
13354
|
```
|
|
12557
13355
|
|
|
13356
|
+
### Options from the content
|
|
13357
|
+
|
|
13358
|
+
`options` can also be a function that loads them, for example an option per category entry. It receives the `graph` to query and the `locale` of the overview, and runs when the overview opens. Filters can match on the entries that link to a listed entry too, with `_referenced` (see [Entries that link here](https://v2.alineacms.com/docs/query/related#entries-that-link-here)):
|
|
13359
|
+
|
|
13360
|
+
File: schema/Products.ts
|
|
13361
|
+
```tsx
|
|
13362
|
+
import {Config, Query} from 'alinea'
|
|
13363
|
+
import {Category, Product} from './Product'
|
|
13364
|
+
|
|
13365
|
+
export const Products = Config.document('Products', {
|
|
13366
|
+
contains: ['Product'],
|
|
13367
|
+
overview: {
|
|
13368
|
+
filters: {
|
|
13369
|
+
category: {
|
|
13370
|
+
label: 'Category',
|
|
13371
|
+
multiple: true,
|
|
13372
|
+
async options({graph, locale}) {
|
|
13373
|
+
const categories = await graph.find({
|
|
13374
|
+
type: Category,
|
|
13375
|
+
locale,
|
|
13376
|
+
select: {id: Query.id, title: Query.title},
|
|
13377
|
+
orderBy: {asc: Query.title}
|
|
13378
|
+
})
|
|
13379
|
+
return Object.fromEntries(
|
|
13380
|
+
categories.map(category => [
|
|
13381
|
+
category.id,
|
|
13382
|
+
{
|
|
13383
|
+
label: category.title,
|
|
13384
|
+
filter: {categories: {includes: {_entry: category.id}}}
|
|
13385
|
+
}
|
|
13386
|
+
])
|
|
13387
|
+
)
|
|
13388
|
+
}
|
|
13389
|
+
},
|
|
13390
|
+
usage: {
|
|
13391
|
+
label: 'Usage',
|
|
13392
|
+
options: {
|
|
13393
|
+
linked: {label: 'Linked from other entries', filter: {_referenced: true}},
|
|
13394
|
+
unused: {label: 'Not linked anywhere', filter: {_referenced: false}}
|
|
13395
|
+
}
|
|
13396
|
+
}
|
|
13397
|
+
}
|
|
13398
|
+
},
|
|
13399
|
+
fields: {}
|
|
13400
|
+
})
|
|
13401
|
+
```
|
|
13402
|
+
|
|
13403
|
+
## Tabs
|
|
13404
|
+
|
|
13405
|
+
`tabs` shows tabs above the listed entries, apart from the filter and sort menu. Each tab has a `label` and an optional `filter`, like the options of a filter. A tab without a filter lists every child. The first tab is selected when the overview opens. The key of the selected tab is kept in the url (`?tab=active`) and the overview remembers it when the editor returns to it. Entries match the filter of the tab, the applied filters and the search terms.
|
|
13406
|
+
|
|
13407
|
+
File: schema/Products.ts
|
|
13408
|
+
```tsx
|
|
13409
|
+
import {Config} from 'alinea'
|
|
13410
|
+
|
|
13411
|
+
export const Products = Config.document('Products', {
|
|
13412
|
+
contains: ['Product'],
|
|
13413
|
+
overview: {
|
|
13414
|
+
tabs: {
|
|
13415
|
+
all: {label: 'All'},
|
|
13416
|
+
active: {label: 'Active', filter: {status: 'active'}},
|
|
13417
|
+
archived: {label: 'Archived', filter: {status: 'archived'}}
|
|
13418
|
+
}
|
|
13419
|
+
},
|
|
13420
|
+
fields: {}
|
|
13421
|
+
})
|
|
13422
|
+
```
|
|
13423
|
+
|
|
12558
13424
|
## Mixed lists
|
|
12559
13425
|
|
|
12560
13426
|
When a parent contains several types, a column can select a different value per type. Key `select` and `sortBy` by the type names of your schema:
|
|
@@ -12640,6 +13506,33 @@ export function ExportProducts({query}: OverviewActionProps) {
|
|
|
12640
13506
|
}
|
|
12641
13507
|
```
|
|
12642
13508
|
|
|
13509
|
+
## Header
|
|
13510
|
+
|
|
13511
|
+
`header` renders a component between the toolbar (and the tabs) and the listed entries, for example totals or a chart of what is listed. It receives `OverviewHeaderProps`: the `workspace`, `root` and `parentId` of the list, the `locale`, the `search` terms, the `filter` of the selected tab and the applied filters (undefined when none applies) and a `query` for the listed entries, without paging. Like `actions`, it takes a component or a path to one: `header: '@/views/ProductTotals'`.
|
|
13512
|
+
|
|
13513
|
+
File: views/ProductTotals.tsx
|
|
13514
|
+
```tsx
|
|
13515
|
+
import {type OverviewHeaderProps, useGraph} from 'alinea/cms'
|
|
13516
|
+
import {Stack, Text} from 'alinea/components'
|
|
13517
|
+
import {useEffect, useState} from 'react'
|
|
13518
|
+
import {Product} from '@/schema/Product'
|
|
13519
|
+
|
|
13520
|
+
export function ProductTotals({query}: OverviewHeaderProps) {
|
|
13521
|
+
const graph = useGraph()
|
|
13522
|
+
const [prices, setPrices] = useState<Array<number | null>>([])
|
|
13523
|
+
useEffect(() => {
|
|
13524
|
+
graph.find({...query, select: Product.price}).then(setPrices)
|
|
13525
|
+
}, [graph, query])
|
|
13526
|
+
const total = prices.reduce<number>((sum, price) => sum + (price ?? 0), 0)
|
|
13527
|
+
return (
|
|
13528
|
+
<Stack direction="row" gap={24}>
|
|
13529
|
+
<Text>{prices.length} products</Text>
|
|
13530
|
+
<Text>€{total} in total</Text>
|
|
13531
|
+
</Stack>
|
|
13532
|
+
)
|
|
13533
|
+
}
|
|
13534
|
+
```
|
|
13535
|
+
|
|
12643
13536
|
## Entry tables in your own views
|
|
12644
13537
|
|
|
12645
13538
|
`EntryTable` from `alinea/cms` renders a table of entries with the columns of an overview: the headers sort and a row opens its entry. Use it in a `Field.view` section, or any other custom view, to show related entries. Here a brand lists its products:
|
|
@@ -12682,7 +13575,7 @@ export function BrandProducts() {
|
|
|
12682
13575
|
`EntryTable` takes:
|
|
12683
13576
|
|
|
12684
13577
|
- `type`: list entries of this type, or of an array of types.
|
|
12685
|
-
- `filter`: a query [filter](https://v2.alineacms.com/docs/query/filtering), such as `{brand: {has: {_entry: entry.id}}}`.
|
|
13578
|
+
- `filter`: a query [filter](https://v2.alineacms.com/docs/query/filtering), such as `{brand: {has: {_entry: entry.id}}}`. Conditions on related entries work too: `{_referenced: Query.referencedBy({type: Product, filter: {stock: {gt: 0}}})}` lists the entries that a product in stock links to.
|
|
12686
13579
|
- `workspace`, `root`, `parentId`: only list entries in this workspace or root, or the children of this entry (`null` for the top level).
|
|
12687
13580
|
- `overview`: the columns and default order to use: an overview object, or the type or root that configures one. Defaults to the overview of the root or type that contains `type`.
|
|
12688
13581
|
- `columns`: show these columns instead of those of the overview.
|
|
@@ -12743,7 +13636,7 @@ Prefer this over a type with a hidden title and a single `Field.view`: that crea
|
|
|
12743
13636
|
|
|
12744
13637
|
Two other options replace parts of the dashboard:
|
|
12745
13638
|
|
|
12746
|
-
- A type's `view` replaces the
|
|
13639
|
+
- A type's `view` replaces the form of entries of that type, below the entry header. It receives `TypeViewProps`: `{type}`.
|
|
12747
13640
|
- A type's `defaultView: 'overview'` opens the overview of an entry's children first, instead of its form.
|
|
12748
13641
|
|
|
12749
13642
|
## Upgrading from Alinea 1.x
|
|
@@ -12812,9 +13705,11 @@ The URL of the API route that serves the dashboard and media files: the route th
|
|
|
12812
13705
|
|
|
12813
13706
|
The URL of your website, either one string or one per environment: `{development, production}`, picked by `NODE_ENV`. A value without a protocol gets `https://`.
|
|
12814
13707
|
|
|
12815
|
-
- `alinea build` exits with `
|
|
13708
|
+
- It is required for production builds when `handlerUrl` is relative: `alinea build` exits, and queries on your site throw, with `Missing baseUrl in Alinea config: set baseUrl.production to the url of your site, …`. An absolute `handlerUrl` doesn't need it.
|
|
12816
13709
|
- In production your site reaches the handler at `handlerUrl` resolved against this URL, to check for new content. That includes `next start` on your machine, which talks to the production URL.
|
|
12817
13710
|
|
|
13711
|
+
Server renders have no request to take the origin from (static pages and pages from `generateStaticParams` are rendered without one, and reading the request headers would make every route dynamic), so Alinea doesn't guess it. Set `baseUrl.production`, or make `handlerUrl` absolute.
|
|
13712
|
+
|
|
12818
13713
|
Most projects read the production URL from an environment variable, see [Environment variables](#environment-variables) below.
|
|
12819
13714
|
|
|
12820
13715
|
## Dashboard options
|
|
@@ -13016,9 +13911,11 @@ Options
|
|
|
13016
13911
|
--fix Any missing or incorrect properties will be overwritten by their default
|
|
13017
13912
|
```
|
|
13018
13913
|
|
|
13019
|
-
`--config` and `--dir` work as for `alinea dev`. `--fix` rewrites content files whose data doesn't match the schema, filling missing or invalid properties with their defaults, and exits without running the command after `--`. Commit the result.
|
|
13914
|
+
`--config` and `--dir` work as for `alinea dev`. `--fix` rewrites content files whose data doesn't match the schema, filling missing or invalid properties with their defaults, and points media files whose `location` is a url at the file of the same name in the `mediaDir`. It exits without running the command after `--`. Commit the result.
|
|
13915
|
+
|
|
13916
|
+
The build stops with an error when the config can't be loaded, when `handlerUrl` is missing or when `baseUrl` has no production URL while `handlerUrl` is relative, see [Configuration](https://v2.alineacms.com/docs/configuration).
|
|
13020
13917
|
|
|
13021
|
-
|
|
13918
|
+
`alinea dev` and `alinea build` warn about media files whose `location` is a url, which can't be served (some 1.x saves wrote the upload url there). The warning names the file and, when the `mediaDir` holds a file of the same name, suggests `alinea build --fix` to point the location at it.
|
|
13022
13919
|
|
|
13023
13920
|
## Deploying
|
|
13024
13921
|
|