Compare commits

..

188 Commits

Author SHA1 Message Date
James
f938dd718f chore(release): v1.1.25 2022-11-15 08:39:43 -05:00
James Mikrut
94b2ef1613 Merge pull request #1411 from jacobsfletch/master
Saves tabs to user preferences
2022-11-15 08:06:03 -05:00
Jacob Fletcher
71a6c58b27 chore: cleans up handleTabChange async callback 2022-11-15 07:40:39 -05:00
Jacob Fletcher
35426eef36 feat: updates tab preference keys 2022-11-14 18:30:18 -05:00
Jacob Fletcher
32833ec571 Merge branch 'payloadcms:master' into master 2022-11-14 17:49:36 -05:00
Jacob Fletcher
5eb8e4a28f feat: saves tab index to user preferences 2022-11-14 17:49:18 -05:00
Thomas Ghysels
0f27b103b4 feat: let textarea grow based on value (#1398)
* feat: autogrowing textarea

* feat: sets textarea min-height and reduces height on mobile

Co-authored-by: Jessica Boezwinkle <jessica@trbl.design>
2022-11-14 16:26:22 -05:00
James
d103f6c94f fix: ensures admin components is defaulted 2022-11-14 15:46:34 -05:00
Alberto Maghini
a345ef0d31 feat: admin UI logout extensibility (#1274)
* added Logout documentation

* updated type and schema

* updated logout component, route and inactivityRoute references

* added custom Logout component into test admin instance

* fixed windows path management

* added dotenv usage

* added check on testSuiteDir and provided more meaningful error message

* fixed object destructure

* updated from logout.route to logoutRoute

* extracted getSanitizedLogoutRoutes method

* added unit tests

* updated references

* updated doc

* reviewed casing and added defaults

* updated usage

* restored workers previous value

* fixed config validation

* updated docs and schema

* updated reference to logoutRoute and inactivityRoute

* updated test ref

Co-authored-by: Alberto Maghini (MSC Technology Italia) <alberto.maghini@msc.com>
Co-authored-by: Alberto Maghini <alberto@newesis.com>
2022-11-14 14:55:31 -05:00
Thomas Ghysels
4d8cc97475 fix: add slug to DocumentInfo context (#1389) 2022-11-14 14:54:05 -05:00
James
7556b54017 chore: fixes TS error in UploadGallery 2022-11-14 14:49:31 -05:00
Feng Sun
5e8a8b2df9 fix: adds unique key to upload cards to prevent old images being shown while navigating to new page 2022-11-14 14:48:08 -05:00
James Mikrut
244fb63c6d Update what-is-payload.mdx 2022-11-14 14:19:48 -05:00
James Mikrut
84f01e8836 Update ui.mdx 2022-11-14 10:11:28 -05:00
Elliot DeNolf
cdaa8cc29f fix: global afterRead and afterChange execution (#1405) 2022-11-14 08:18:58 -05:00
James Mikrut
7356d8977f Update overview.mdx 2022-11-13 20:39:26 -05:00
James
a3959ca5d8 chore(release): v1.1.24 2022-11-13 19:21:13 -05:00
Thomas Ghysels
216b9f88d9 fix: cursor jumping while typing in inputs
* Revert "feat: optimizes field performance by storing internal values in useField hook"

This reverts commit 66210b856b.

* fix: Lightweight fix for cursor jump issue

Resolves #1393
2022-11-13 19:14:16 -05:00
James
183cd9a0be chore: updates docs to reflect new website public images folder 2022-11-13 17:41:39 -05:00
James
5f5c7ba7bf chore(release): v1.1.23 2022-11-12 18:58:34 -05:00
James
66210b856b feat: optimizes field performance by storing internal values in useField hook 2022-11-12 18:46:31 -05:00
James
2f684040fc fix: #1361, ensures collection auth depth works while retrieving static assets 2022-11-12 15:44:24 -05:00
James
2364476689 chore(release): v1.1.22 2022-11-12 14:49:20 -05:00
James
7136db4c71 fix: #1360, relationship field onMenuScrollToBottom not working in some browsers 2022-11-12 14:44:00 -05:00
James
562fccce05 docs: resolves #1273, inaccurate docs regarding local API overrideAccess 2022-11-12 14:22:23 -05:00
James
35f91b038b fix: 1309, duplicative logout in admin UI 2022-11-12 14:19:08 -05:00
James
eb0023e961 fix: #1358, allows listSearchableFields to work when indicated fields are nested 2022-11-12 13:44:30 -05:00
James
1d76e973bb fix: #1367, allows custom global components within schema validation 2022-11-12 13:08:10 -05:00
James
3f28a69959 fix: #1353, ensures errors returned from server make their way to UI 2022-11-12 12:55:30 -05:00
James
77792327f1 Merge branch 'master' of github.com:payloadcms/payload 2022-11-11 12:04:01 -07:00
James
86855d68f6 fix: #1357, nested arrays and blocks sometimes not allowing save 2022-11-11 12:03:39 -07:00
Nick Borko
cfef68f364 fix: fixed GraphQL Access query resolver to return the correct data (#1339) 2022-11-11 04:25:46 -08:00
Nut Pinyo
32b8f46bf2 docs: field hook example configuration (#1329) 2022-11-11 04:19:18 -08:00
James
cc5fa943ad Merge branch 'master' of github.com:payloadcms/payload 2022-11-11 04:12:48 -08:00
James
9da9d38aed docs: adds useEditDepth to docs 2022-11-11 04:12:43 -08:00
Feng Sun
d90ca777db chore: export useEditDepth (#1350) 2022-11-11 04:09:06 -08:00
James
e4b4931dba chore(release): v1.1.21 2022-11-05 12:10:30 -04:00
James
93acea9d7f chore: ensures when array / block rows are added, new row count is properly calculated 2022-11-05 12:05:34 -04:00
James
f883e04ee1 chore(release): v1.1.20 2022-11-05 11:45:07 -04:00
James
bb51a54ebe Merge branch 'master' of github.com:payloadcms/payload 2022-11-05 11:38:02 -04:00
James
483adf08c4 feat: optimizes blocks and arrays by removing some additional rerenders 2022-11-05 11:37:55 -04:00
Elliot DeNolf
4a78f7d3c3 chore: change discord readme badge to blurple 2022-11-04 10:31:42 -04:00
James
78c2306b73 Merge branch 'master' of github.com:payloadcms/payload 2022-11-01 17:51:03 -04:00
James
ddfb011904 docs: quick fix to strong tag 2022-11-01 17:50:55 -04:00
James Mikrut
d1c20e4fef Update CHANGELOG.md 2022-10-31 18:23:34 -04:00
James
a5139072c8 chore(release): v1.1.19 2022-10-31 18:23:10 -04:00
James
e004682799 Merge branch 'master' of github.com:payloadcms/payload 2022-10-31 17:51:00 -04:00
James
c651835061 fix: #1318, improves popup positioning and logic 2022-10-31 17:50:52 -04:00
Elliot DeNolf
2255ebb64a feat: revert enforce kebab-case slugs (#1322) (#1325) 2022-10-31 16:10:11 -04:00
James
e2ec2f7b97 fix: #1311, select existing upload modal always updates state 2022-10-31 15:40:18 -04:00
James
00196a8631 chore: ensures form is modified when rows are moved, fixes #1314 2022-10-31 13:25:19 -04:00
James
2a09f15a15 fix: #1307, #1321 - bug with disableFormData and blocks field 2022-10-31 13:21:16 -04:00
Elliot DeNolf
0420b6dc27 feat: enforce kebab-case slugs (#1322) 2022-10-31 11:11:00 -04:00
Dan Ribbens
10c30260dd docs: fix listSearchableFields anchor (#1297) 2022-10-31 09:44:49 -04:00
Elliot DeNolf
25000261bd fix: custom pino logger options (#1299) 2022-10-26 15:08:52 -04:00
Elliot DeNolf
bb82cdcef4 docs: better inline docs for InitOptions 2022-10-26 10:25:06 -04:00
Elliot DeNolf
027dff8363 chore(release): v1.1.18 2022-10-25 12:30:47 -04:00
James
31ca1ab379 chore: ensures defaultMaxTextLength is optional 2022-10-25 12:23:45 -04:00
Elliot DeNolf
33c1f287f3 chore(release): v1.1.17 2022-10-25 11:09:30 -04:00
Elliot DeNolf
cd4861afda chore: move release-it back to dev deps 2022-10-25 11:03:13 -04:00
James
9f56ac182f Merge branch 'master' of github.com:payloadcms/payload 2022-10-25 11:00:48 -04:00
James
3301f59822 fix: enforces depth: 0 in graphql resolvers 2022-10-25 11:00:38 -04:00
James
f9ca3a9f96 chore: fixes bad field name 2022-10-25 11:00:22 -04:00
Elliot DeNolf
3ba7594a65 chore: update release-it plugin 2022-10-25 10:31:37 -04:00
TomDo1234
6a1b25ab30 feat: adds default max length for text-based fields
* feat: Added to types.ts the default Max Field Length

* feat: Added the defaultMaxFieldLength to the schema.ts

* feat: applying defaultMaxFieldLength to 3 validators

* feat: renamed defaultMaxFieldLength to defaultMaxTextLength , adding defaultMax and min nums

* feat: validating numbers with new defaultminnum and defaultmaxnum

* feat: FIXED BUG, do not return an error message on the defaultmaxnum and minnum override checks

* Added test fields

* Eslint compliance

* feat : eslint compliacnce

* Added tests, though a reasonable payload config needs to be imported to them

* Removed my failed jest tests, relying on the yarn dev test instead

* Increased default num max and min range to JS safe integer

* Jmi suggestions

* feat: removing the superfluous number max and min default

* Added test for max text field

Co-authored-by: Tom Do <tom@iifuture.com>
Co-authored-by: TomDoFuture <108644869+TomDoFuture@users.noreply.github.com>
2022-10-24 18:57:45 -04:00
James
17dbbc7775 fix: group + group styles within collapsible 2022-10-24 18:56:56 -04:00
James
7e25abf87a chore: type fixes 2022-10-24 12:26:25 -04:00
James
0591dfd05b chore: typo 2022-10-24 12:18:03 -04:00
Will Laeri
17610adf36 chore: upgrade vulnerable dependencies (#1228)
* upgrade vulnerable deps

* removed specified node/yarn versions
2022-10-24 12:14:48 -04:00
Will Laeri
91814777b0 feat: specify node 14+ and yarn classic LTS (#1240) 2022-10-24 12:14:22 -04:00
Daniel Söderling
09d793926d feat: added beforeLogin hook (#1289) 2022-10-24 12:05:12 -04:00
James
a9f2f0ec03 fix: #1290, renders more than one rich text leaf where applicable 2022-10-24 12:03:06 -04:00
James
66bf8c3cbd fix: #1286, uses defaultDepth in graphql rich text depth 2022-10-24 11:31:09 -04:00
James
3967c1233f fix: #1291, add inline relationship drafts 2022-10-24 11:23:01 -04:00
James
c929725dd5 fix: ensures field updates when disableFormData changes 2022-10-24 08:56:08 -04:00
James
9c6098b191 chore(release): v1.1.16 2022-10-21 15:48:40 -04:00
James
6daab398da Merge branch 'master' of github.com:payloadcms/payload 2022-10-21 15:39:36 -04:00
James
36ef3789fb fix: obscure bug where upload collection has upload field relating to itself 2022-10-21 15:39:29 -04:00
Hung Vu
14cbf2f079 docs: correction to demo code of radio field (#1266) 2022-10-18 14:10:27 -04:00
Dan Ribbens
87bbf4416b Merge pull request #1272 from payloadcms/fix/#1271-index-sortable-fields-regression 2022-10-18 14:05:36 -04:00
Dan Ribbens
785b992c3e fix: indexSortableFields not respected 2022-10-17 20:17:55 -04:00
James
b4695e10b6 chore(release): v1.1.15 2022-10-14 11:38:48 -04:00
James
0b0d971491 fix: ensures svg mime type is always image/svg+xml 2022-10-14 11:33:41 -04:00
James
02af6b90b2 chore(release): v1.1.14 2022-10-14 10:22:14 -04:00
James
2181bc84a1 1.1.13 2022-10-14 10:02:40 -04:00
James
036cd5f831 1.1.12 2022-10-14 10:02:33 -04:00
James
da9825cd99 chore: reverts hiding scrollbars in tabs 2022-10-14 09:53:43 -04:00
James
4a43f95952 fix: hides scrollbar in tabs with overflowing tabs, closes #1259 2022-10-14 09:51:44 -04:00
James
9af9b73132 fix: cleans up draft global action buttons 2022-10-14 09:37:27 -04:00
James
7f7d3dbeef Merge branch 'master' of github.com:payloadcms/payload 2022-10-13 08:28:37 -04:00
James
8ef9206001 chore: properly exports BeforeDuplicate type 2022-10-13 08:17:05 -04:00
James
21ba237135 fix: #1183 - better handling of mime types with svgs + similar files 2022-10-13 08:12:15 -04:00
James Mikrut
3bda163e7b Merge pull request #1241 from jacobsfletch/master
feat: bumps @faceless-ui/modal to v2.0.1
2022-10-12 12:00:22 -04:00
James
e4e4ad1b08 chore: ensures beforeDuplicate works on each locale 2022-10-12 11:59:56 -04:00
James
f7352a7d08 chore(release): v1.1.11 2022-10-12 11:52:48 -04:00
James
6f6f2f8e7b feat: builds beforeDuplicate admin hook, closes #1243 2022-10-12 11:44:41 -04:00
James
5ca5abab42 fix: ensures arrays and blocks mount as disableFormData: true, fixes #1242 2022-10-12 10:52:59 -04:00
Jacob Fletcher
9a7553099c feat: migrates @faceless-ui/modal to v2.0.1 2022-10-12 01:28:17 -04:00
James
55d0c917e6 chore(release): v1.1.10 2022-10-11 13:40:19 -04:00
James
f52daeccf0 docs: adds access control vid to docs 2022-10-11 13:34:35 -04:00
James
6c871c57fc chore(release): v1.1.9 2022-10-11 12:12:40 -04:00
James
5322ada9e6 feat: improves access control typing 2022-10-11 11:46:58 -04:00
James
ee83a50ea9 chore: improves read-only styling of all react-selects 2022-10-11 11:46:44 -04:00
James
f6b19e074c chore: more predictably exports Access type 2022-10-11 09:44:52 -04:00
James
6cc1d9e41b chore(release): v1.1.8 2022-10-11 09:24:07 -04:00
James
74863f9462 chore: workaround for faceless-ui modal types 2022-10-11 09:19:20 -04:00
James
fdcf029da2 chore: adjusts LeaveWithoutSaving z-index 2022-10-11 09:00:19 -04:00
James
3e3d151e4c docs: #1235, broken typescript link 2022-10-11 08:32:27 -04:00
James Mikrut
5da204b152 Merge pull request #1224 from payloadcms/feat/use-context-selector
Feat/use context selector
2022-10-10 18:38:36 -04:00
James
3d6c3f7339 chore: cleanup 2022-10-10 18:38:15 -04:00
James
8d49517004 chore: merge master 2022-10-10 15:12:38 -04:00
James Mikrut
d1c0f2b97b Merge pull request #1230 from payloadcms/feat/add-inline-relationship
feat: adds ability to create related docs while editing another
2022-10-10 15:07:41 -04:00
James
1bc42ae098 chore: tests 2022-10-10 14:56:10 -04:00
James
c6edb7f53a chore: improves design 2022-10-10 13:57:08 -04:00
James
1e048fe037 feat: adds ability to create related docs while editing another 2022-10-09 20:24:38 -04:00
James
8fabdce584 chore: restores old useWatchForm to avoid breaking change 2022-10-07 17:54:35 -04:00
James
5c1a3fabee feat: implements use-context-selector for form field access 2022-10-07 17:41:41 -04:00
James
fe6d30210b Merge branch 'master' of github.com:payloadcms/payload into feat/use-context-selector 2022-10-07 10:09:20 -04:00
James
93f71e621c chore(release): v1.1.7 2022-10-06 18:14:37 -04:00
James Mikrut
39d1a09d5a Merge pull request #1215 from damtzi/chore/credentials-include
chore: Add 'credentials: include' to all fetch calls
2022-10-06 17:55:55 -04:00
Damian Tziamtzis
74ae6fd1d5 chore: Add 'credentials: include' to all fetch calls 2022-10-06 23:14:55 +02:00
James
bbbcf8c869 chore(release): v1.1.6 2022-10-06 17:10:10 -04:00
James
b379666dec chore: improves upload field button aesthetics a bit 2022-10-06 17:04:29 -04:00
James Mikrut
6f40b5c9ab Merge pull request #1175 from bigmistqke/fix/responsive-fileupload-width
Fix: responsive fileupload width
2022-10-06 16:42:25 -04:00
James Mikrut
b329be7dc1 Merge pull request #1214 from payloadcms/fix/#1184
fix: #1184
2022-10-06 16:12:05 -04:00
James
c2ec54a7cb fix: #1184 2022-10-06 16:11:24 -04:00
James
3641dfd38a fix: #1189 2022-10-06 15:42:20 -04:00
James
5bf1354741 chore: fixes tests 2022-10-06 15:23:49 -04:00
James Mikrut
b894b809bf Merge pull request #1212 from payloadcms/fix/#940
Fix/#940
2022-10-06 14:24:51 -04:00
James
a4504ca15b Merge branch 'master' of github.com:payloadcms/payload into fix/#940 2022-10-06 14:23:37 -04:00
James
7926083732 fix: #940 2022-10-06 14:23:08 -04:00
James Mikrut
534cd5ae53 Merge pull request #1211 from jacobsfletch/master
feat: async admin access control
2022-10-06 13:31:01 -04:00
James Mikrut
fb329a99ba Merge pull request #1210 from payloadcms/fix/#1204
fix: #1204
2022-10-06 13:24:42 -04:00
James Mikrut
9e726d9b90 Merge pull request #1174 from payloadcms/fix/#1156-file-uploads-changing-extensions
fix: upload xls renaming ext
2022-10-06 13:23:23 -04:00
James Mikrut
8d065d619d Merge pull request #1124 from payloadcms/feat/sortable-by-default
feat: sort select and relationship fields by default
2022-10-06 13:20:32 -04:00
James Mikrut
cbff1776e7 Merge pull request #1168 from payloadcms/fix/read-only
Fix: read only field styles
2022-10-06 13:20:16 -04:00
nwhitmont
e517695000 docs: +note that collection slug must be in kebab-case, refactor example routes to match (#1176)
Co-authored-by: Nils Whitmont <nwhitmont@genvidtech.com>
2022-10-06 13:18:44 -04:00
James Mikrut
4370cfca0c Merge pull request #1195 from bigmistqke/fix/textarea-resize-vertical
fix: resize textarea only vertically
2022-10-06 13:17:51 -04:00
James Mikrut
4135b618ef Merge pull request #1198 from dsod/fix/resize-images-naming-and-mimetype
use the converted image mimeType for filename and admin interface
2022-10-06 13:17:22 -04:00
Jacob Fletcher
1cfce87549 feat: async admin access control 2022-10-06 13:16:15 -04:00
James Mikrut
c48283ac1d Merge pull request #1201 from christian-reichart/fix/sibling-data-in-after-read
fix: sibling data in after read hook
2022-10-06 13:15:52 -04:00
James Mikrut
328be3e4bc Merge pull request #1206 from payloadcms/style/color-scheme
fix: system dark scrollbars
2022-10-06 13:14:54 -04:00
James
b4becd1493 fix: #1204 2022-10-06 13:11:25 -04:00
James
95fac0bd62 chore: wip 2022-10-05 15:51:22 -04:00
Jarrod Flesch
a30d9dc1d7 fix(style): system dark scrollbars 2022-10-05 15:33:41 -04:00
Christian Reichart
7bfcefbfea fix sibling data in after read hook 2022-10-04 14:39:09 +02:00
dsod
131b2796e7 now uses the converted image mimeType for file extension and in the admin interface 2022-10-03 19:24:14 +02:00
Elliot DeNolf
debcb003bb docs: clarify api key auth usage 2022-10-03 08:41:23 -04:00
bigmistqke
6e1dfff1b8 fix: resize textarea only vertically 2022-10-02 17:37:27 +02:00
Jarrod Flesch
a9ebb71a09 Merge branch 'master' into fix/read-only 2022-09-30 10:47:34 -04:00
James
3e34e5216f chore(release): v1.1.5 2022-09-29 17:56:44 -04:00
James
2400c58219 chore: addresses more flaky tests 2022-09-29 17:52:51 -04:00
James
90d504526c chore: adds more delay to flaky test 2022-09-29 17:17:14 -04:00
Jarrod Flesch
c97d4f9545 Merge branch 'master' into fix/read-only 2022-09-29 12:54:02 -04:00
Jarrod Flesch
09a8144f3c fix: richText e2e test, specific selectors 2022-09-29 12:53:16 -04:00
Jarrod Flesch
00ef1700ae fix: ajusts how disabled states are being set on anchors and buttons 2022-09-29 11:49:25 -04:00
James
3e03b2b5df Merge branch 'master' of github.com:payloadcms/payload 2022-09-29 11:11:14 -04:00
James
974f79e57e chore: sends 204 on GraphQL OPTIONS requests 2022-09-29 11:11:02 -04:00
James
34f42083b5 chore: rolls back changes to useThrottledEffect 2022-09-29 10:26:06 -04:00
Elliot DeNolf
c0cae1e834 chore: reorder bug report template 2022-09-29 09:27:33 -04:00
James
3ce8ee4661 fix: bug in useThrottledEffect 2022-09-28 17:40:31 -04:00
bigmistqke
f9feff58d6 add flex-wrap: wrap to upload__wrap 2022-09-26 21:03:28 +02:00
bigmistqke
73848b6037 fix: remove min-width from fileupload 2022-09-26 20:51:45 +02:00
Dan Ribbens
7fd8124df6 fix: upload xls renaming ext 2022-09-26 08:19:58 -04:00
James Mikrut
1c77455403 Merge pull request #1169 from payloadcms/docs/test-cache
Improves contributing doc
2022-09-24 12:06:42 -07:00
James
051a0fad84 chore(release): v1.1.4 2022-09-23 20:15:52 -07:00
Jarrod Flesch
8e53ef47a0 chore: adds note about clearing the node module cache when switching test directories 2022-09-23 15:23:28 -04:00
Jarrod Flesch
918130486e fix: styles readOnly RichTextEditor, removes interactivity within when readOnly 2022-09-23 14:34:02 -04:00
Jarrod Flesch
b454811698 fix: threads readOnly to ReactSelect 2022-09-23 13:17:11 -04:00
James Mikrut
f87c68f310 Merge pull request #1147 from abaco/fix/refine-relationship-typegen
fix: refine type generation for relationships
2022-09-23 09:51:36 -07:00
James
25006d44e8 Merge branch 'master' of github.com:payloadcms/payload 2022-09-23 09:49:06 -07:00
James Mikrut
d8e51dd200 Merge pull request #1157 from payloadcms/docs/cell-component-props
docs: cell component props and example
2022-09-23 09:46:49 -07:00
James Mikrut
f54210a528 Merge pull request #1161 from jacobsfletch/master
feat: supports root endpoints
2022-09-23 09:46:30 -07:00
James Mikrut
96dab15cd1 Merge pull request #1163 from payloadcms/fix/nested-fields-permissions
fix: field level access for nested fields
2022-09-23 09:44:02 -07:00
James Mikrut
4126843619 Merge pull request #1165 from jacobsfletch/docs/webpack-cache
docs: adds tip for clearing webpack cache when aliasing server modules
2022-09-23 09:42:35 -07:00
James
e0238ad393 chore: updates sass 2022-09-23 09:42:10 -07:00
Jacob Fletcher
aa0302c05e docs: adds tip for clearing webpack cache when aliasing server modules 2022-09-23 12:24:51 -04:00
Jacob Fletcher
1040ad2cfe Merge branch 'payloadcms:master' into master 2022-09-23 12:23:57 -04:00
Dan Ribbens
c64f15d4d9 test: field level access for nested fields 2022-09-23 03:26:26 -04:00
Dan Ribbens
22ea98ca33 fix: field level access for nested fields 2022-09-22 21:37:02 -04:00
Jacob Fletcher
75bab716d1 chore: adds root endpoint test 2022-09-22 13:23:42 -04:00
Dan Ribbens
52a8e9624c docs: plugins typo 2022-09-22 12:19:18 -04:00
Jacob Fletcher
52cd3b4a7e feat: supports root endpoints 2022-09-22 10:49:49 -04:00
addison-codes
cc63167307 docs: fix highlighting 2022-09-22 00:51:25 -04:00
Dan Ribbens
314671b3b7 docs: cell component props and example 2022-09-21 14:20:01 -04:00
Dario Aprea
ef83bdb709 fix: refine type generation for relationships 2022-09-20 16:27:09 +02:00
James
f9b1b1fe7f docs: adds Link example in rich text 2022-09-13 21:25:08 -07:00
Elliot DeNolf
813c46c86d feat: sort select and relationship fields by default 2022-09-13 20:06:12 -07:00
219 changed files with 6933 additions and 4148 deletions

View File

@@ -8,20 +8,21 @@ labels: 'possible-bug'
<!--- Provide a general summary of the issue in the Title above -->
## Expected Behavior
<!--- Tell us what should happen -->
## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
## Expected Behavior
<!--- Tell us what you expected happen -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- Optional. If familiar with the codebase, suggest a fix/reason for the bug. -->
## Steps to Reproduce
<!--- Steps to reproduce this bug. Include any code, if relevant -->
1.
2.
3.

1
.node-version Normal file
View File

@@ -0,0 +1 @@
v16.14.2

View File

@@ -1,3 +1,187 @@
## [1.1.25](https://github.com/payloadcms/payload/compare/v1.1.24...v1.1.25) (2022-11-15)
### Bug Fixes
* add slug to DocumentInfo context ([#1389](https://github.com/payloadcms/payload/issues/1389)) ([4d8cc97](https://github.com/payloadcms/payload/commit/4d8cc97475c73e5131699ef03dca275a17535a25))
* adds unique key to upload cards to prevent old images being shown while navigating to new page ([5e8a8b2](https://github.com/payloadcms/payload/commit/5e8a8b2df9af435f0df8a8a07dddf7dcc24cf9ac))
* ensures admin components is defaulted ([d103f6c](https://github.com/payloadcms/payload/commit/d103f6c94f91b5359aea722c2d7781bf144f6a26))
* global afterRead and afterChange execution ([#1405](https://github.com/payloadcms/payload/issues/1405)) ([cdaa8cc](https://github.com/payloadcms/payload/commit/cdaa8cc29f58308a387375ec41eafd0d38b13bcb))
### Features
* admin UI logout extensibility ([#1274](https://github.com/payloadcms/payload/issues/1274)) ([a345ef0](https://github.com/payloadcms/payload/commit/a345ef0d3179000a2930f8b09886e06fd0801d21))
* let textarea grow based on value ([#1398](https://github.com/payloadcms/payload/issues/1398)) ([0f27b10](https://github.com/payloadcms/payload/commit/0f27b103b44935480b8ffe17427fc5ed05b92446))
* saves tab index to user preferences ([5eb8e4a](https://github.com/payloadcms/payload/commit/5eb8e4a28f34a1c51790d4eabfb21606b7fb41c6))
* updates tab preference keys ([35426ee](https://github.com/payloadcms/payload/commit/35426eef3620f312abafdc1d3869273d674caaab))
## [1.1.24](https://github.com/payloadcms/payload/compare/v1.1.23...v1.1.24) (2022-11-14)
### Bug Fixes
* cursor jumping while typing in inputs ([216b9f8](https://github.com/payloadcms/payload/commit/216b9f88d988c692d6acdf920ee4dbb9903020ae)), closes [#1393](https://github.com/payloadcms/payload/issues/1393)
## [1.1.23](https://github.com/payloadcms/payload/compare/v1.1.22...v1.1.23) (2022-11-12)
### Bug Fixes
* [#1361](https://github.com/payloadcms/payload/issues/1361), ensures collection auth depth works while retrieving static assets ([2f68404](https://github.com/payloadcms/payload/commit/2f684040fc9ca717d48b0d95cbd3468c35973993))
### Features
* optimizes field performance by storing internal values in useField hook ([66210b8](https://github.com/payloadcms/payload/commit/66210b856b97139f9959fac47154bca44f0a4de0))
## [1.1.22](https://github.com/payloadcms/payload/compare/v1.1.21...v1.1.22) (2022-11-12)
### Bug Fixes
* [#1353](https://github.com/payloadcms/payload/issues/1353), ensures errors returned from server make their way to UI ([3f28a69](https://github.com/payloadcms/payload/commit/3f28a69959be9c98869f81bcd379b8c7cd505a12))
* [#1357](https://github.com/payloadcms/payload/issues/1357), nested arrays and blocks sometimes not allowing save ([86855d6](https://github.com/payloadcms/payload/commit/86855d68f65dfadbf51050bdaf6a28c3220add6f))
* [#1358](https://github.com/payloadcms/payload/issues/1358), allows listSearchableFields to work when indicated fields are nested ([eb0023e](https://github.com/payloadcms/payload/commit/eb0023e9617894873fe75748de187d85279498c8))
* [#1360](https://github.com/payloadcms/payload/issues/1360), relationship field onMenuScrollToBottom not working in some browsers ([7136db4](https://github.com/payloadcms/payload/commit/7136db4c718b70833fa75f5c8e9ae596298b3aa9))
* [#1367](https://github.com/payloadcms/payload/issues/1367), allows custom global components within schema validation ([1d76e97](https://github.com/payloadcms/payload/commit/1d76e973bb8e6e33e40b469bd410042ae4b90e2e))
* 1309, duplicative logout in admin UI ([35f91b0](https://github.com/payloadcms/payload/commit/35f91b038b66d74468dad250dbe7cbf1ea88b444))
* fixed GraphQL Access query resolver to return the correct data ([#1339](https://github.com/payloadcms/payload/issues/1339)) ([cfef68f](https://github.com/payloadcms/payload/commit/cfef68f36477e34b9943d9334c65fa46ee3eb339))
## [1.1.21](https://github.com/payloadcms/payload/compare/v1.1.20...v1.1.21) (2022-11-05)
## [1.1.20](https://github.com/payloadcms/payload/compare/v1.1.19...v1.1.20) (2022-11-05)
### Features
* optimizes blocks and arrays by removing some additional rerenders ([483adf0](https://github.com/payloadcms/payload/commit/483adf08c4131d0401e47ec45d72200b9dc60de2))
## [1.1.19](https://github.com/payloadcms/payload/compare/v1.1.18...v1.1.19) (2022-10-31)
### Bug Fixes
* [#1307](https://github.com/payloadcms/payload/issues/1307), [#1321](https://github.com/payloadcms/payload/issues/1321) - bug with disableFormData and blocks field ([2a09f15](https://github.com/payloadcms/payload/commit/2a09f15a158ff30e89c5454f81aa140448f15d30))
* [#1311](https://github.com/payloadcms/payload/issues/1311), select existing upload modal always updates state ([e2ec2f7](https://github.com/payloadcms/payload/commit/e2ec2f7b97ed308c4ff7deefbc58cf0df6ff0602))
* [#1318](https://github.com/payloadcms/payload/issues/1318), improves popup positioning and logic ([c651835](https://github.com/payloadcms/payload/commit/c6518350617d14818dfc537b5b0a147274c1119b))
* custom pino logger options ([#1299](https://github.com/payloadcms/payload/issues/1299)) ([2500026](https://github.com/payloadcms/payload/commit/25000261bd6ecb0f05ae79de9a0693078a0e3e0d))
## [1.1.18](https://github.com/payloadcms/payload/compare/v1.1.17...v1.1.18) (2022-10-25)
## [1.1.17](https://github.com/payloadcms/payload/compare/v1.1.16...v1.1.17) (2022-10-25)
### Bug Fixes
* [#1286](https://github.com/payloadcms/payload/issues/1286), uses defaultDepth in graphql rich text depth ([66bf8c3](https://github.com/payloadcms/payload/commit/66bf8c3cbd080ee5a28b7af521d427d3aae59ba2))
* [#1290](https://github.com/payloadcms/payload/issues/1290), renders more than one rich text leaf where applicable ([a9f2f0e](https://github.com/payloadcms/payload/commit/a9f2f0ec03383ef4c3ef3ba98274b0abaaf962ed))
* [#1291](https://github.com/payloadcms/payload/issues/1291), add inline relationship drafts ([3967c12](https://github.com/payloadcms/payload/commit/3967c1233fda00b48e9df15276502a6b14b737ff))
* enforces depth: 0 in graphql resolvers ([3301f59](https://github.com/payloadcms/payload/commit/3301f598223d517ac310909bb74e455891c27693))
* ensures field updates when disableFormData changes ([c929725](https://github.com/payloadcms/payload/commit/c929725dd565de08871dad655442ee9ac4f29dd5))
* group + group styles within collapsible ([17dbbc7](https://github.com/payloadcms/payload/commit/17dbbc77757a7cd6e517bac443859561fee86e32))
### Features
* added beforeLogin hook ([#1289](https://github.com/payloadcms/payload/issues/1289)) ([09d7939](https://github.com/payloadcms/payload/commit/09d793926dbb642bbcb6ab975735d069df355a8a))
* adds default max length for text-based fields ([6a1b25a](https://github.com/payloadcms/payload/commit/6a1b25ab302cbdf7f312012b29b78288815810af))
* specify node 14+ and yarn classic LTS ([#1240](https://github.com/payloadcms/payload/issues/1240)) ([9181477](https://github.com/payloadcms/payload/commit/91814777b0bf3830c4a468b76783ff6f42ad824a))
## [1.1.16](https://github.com/payloadcms/payload/compare/v1.1.15...v1.1.16) (2022-10-21)
### Bug Fixes
* indexSortableFields not respected ([785b992](https://github.com/payloadcms/payload/commit/785b992c3ea31f7818f1c87c816b8b8de644851d))
* obscure bug where upload collection has upload field relating to itself ([36ef378](https://github.com/payloadcms/payload/commit/36ef3789fbe00cafe8b3587d6c370e28efd5a187))
## [1.1.15](https://github.com/payloadcms/payload/compare/v1.1.14...v1.1.15) (2022-10-14)
### Bug Fixes
* ensures svg mime type is always image/svg+xml ([0b0d971](https://github.com/payloadcms/payload/commit/0b0d9714917b1a56fb899a053e2e35c878a00992))
## [1.1.14](https://github.com/payloadcms/payload/compare/v1.1.13...v1.1.14) (2022-10-14)
## [1.1.11](https://github.com/payloadcms/payload/compare/v1.1.10...v1.1.11) (2022-10-12)
### Bug Fixes
* ensures arrays and blocks mount as disableFormData: true, fixes [#1242](https://github.com/payloadcms/payload/issues/1242) ([5ca5aba](https://github.com/payloadcms/payload/commit/5ca5abab422ad1cdb1b449a8298f439c57dda464))
### Features
* builds beforeDuplicate admin hook, closes [#1243](https://github.com/payloadcms/payload/issues/1243) ([6f6f2f8](https://github.com/payloadcms/payload/commit/6f6f2f8e7b83821ae2f2d30d08460439746cc0c6))
## [1.1.10](https://github.com/payloadcms/payload/compare/v1.1.9...v1.1.10) (2022-10-11)
## [1.1.9](https://github.com/payloadcms/payload/compare/v1.1.8...v1.1.9) (2022-10-11)
### Features
* improves access control typing ([5322ada](https://github.com/payloadcms/payload/commit/5322ada9e690544c4864abba202a14ec1f2f5e9d))
## [1.1.8](https://github.com/payloadcms/payload/compare/v1.1.7...v1.1.8) (2022-10-11)
### Features
* adds ability to create related docs while editing another ([1e048fe](https://github.com/payloadcms/payload/commit/1e048fe03787577fe4d584cec9c2d7c78bc90a17))
* implements use-context-selector for form field access ([5c1a3fa](https://github.com/payloadcms/payload/commit/5c1a3fabeef48b78f173af084f9117515e1297ba))
## [1.1.7](https://github.com/payloadcms/payload/compare/v1.1.6...v1.1.7) (2022-10-06)
## [1.1.6](https://github.com/payloadcms/payload/compare/v1.1.5...v1.1.6) (2022-10-06)
### Bug Fixes
* [#1184](https://github.com/payloadcms/payload/issues/1184) ([c2ec54a](https://github.com/payloadcms/payload/commit/c2ec54a7cbd8cd94bcd4a68d885e35986fec7f18))
* [#1189](https://github.com/payloadcms/payload/issues/1189) ([3641dfd](https://github.com/payloadcms/payload/commit/3641dfd38a147b24e0e3ef93a125b12ad7763f66))
* [#1204](https://github.com/payloadcms/payload/issues/1204) ([b4becd1](https://github.com/payloadcms/payload/commit/b4becd1493d55aae887008ab573ab710c400103a))
* [#940](https://github.com/payloadcms/payload/issues/940) ([7926083](https://github.com/payloadcms/payload/commit/7926083732fbaec78d87f67742cdbd8bd00cd48a))
* ajusts how disabled states are being set on anchors and buttons ([00ef170](https://github.com/payloadcms/payload/commit/00ef1700ae41e68ff0831a587bf3f09fe6c2c966))
* remove min-width from fileupload ([73848b6](https://github.com/payloadcms/payload/commit/73848b603790b3c3d8ad8c9dac81b33c0b65fc7e))
* resize textarea only vertically ([6e1dfff](https://github.com/payloadcms/payload/commit/6e1dfff1b8195a1f81e6ea6ccf3b36dd5359c039))
* richText e2e test, specific selectors ([09a8144](https://github.com/payloadcms/payload/commit/09a8144f3cc63f7ec15fd75f51b8ac8d0cf3f1b5))
* styles readOnly RichTextEditor, removes interactivity within when readOnly ([9181304](https://github.com/payloadcms/payload/commit/918130486e1e38a3d57fb993f466207209c5c0bb))
* **style:** system dark scrollbars ([a30d9dc](https://github.com/payloadcms/payload/commit/a30d9dc1d70340cc6c5ac5b3415a6f57bec117ae))
* threads readOnly to ReactSelect ([b454811](https://github.com/payloadcms/payload/commit/b454811698c7ea0cee944ed50030c13163cf72c9))
* upload xls renaming ext ([7fd8124](https://github.com/payloadcms/payload/commit/7fd8124df68d208813de46172c5cd3f479b9b8be))
### Features
* async admin access control ([1cfce87](https://github.com/payloadcms/payload/commit/1cfce8754947487e6c598ed5bc881526295acabf))
* sort select and relationship fields by default ([813c46c](https://github.com/payloadcms/payload/commit/813c46c86d86f8b0a3ba7280d31f24e844c916b6))
## [1.1.5](https://github.com/payloadcms/payload/compare/v1.1.4...v1.1.5) (2022-09-29)
### Bug Fixes
* bug in useThrottledEffect ([3ce8ee4](https://github.com/payloadcms/payload/commit/3ce8ee4661bfa3825c5b8c41232d5da57f7591ed))
## [1.1.4](https://github.com/payloadcms/payload/compare/v1.1.3...v1.1.4) (2022-09-24)
### Bug Fixes
* field level access for nested fields ([22ea98c](https://github.com/payloadcms/payload/commit/22ea98ca33770a0ec6652f814726454abb6da24e))
* refine type generation for relationships ([ef83bdb](https://github.com/payloadcms/payload/commit/ef83bdb709ebde008b90930a6875b24f042a41b0))
### Features
* supports root endpoints ([52cd3b4](https://github.com/payloadcms/payload/commit/52cd3b4a7ed9bc85e93d753a3aaf190489ca98cd))
## [1.1.3](https://github.com/payloadcms/payload/compare/v1.1.2...v1.1.3) (2022-09-16)
@@ -1934,4 +2118,4 @@ If none of your collections or globals should be publicly exposed, you don't nee
* add blind index for encrypting API Keys ([9a1c1f6](https://github.com/payloadcms/payload/commit/9a1c1f64c0ea0066b679195f50e6cb1ac4bf3552))
* add license key to access routej ([2565005](https://github.com/payloadcms/payload/commit/2565005cc099797a6e3b8995e0984c28b7837e82))
## [0.0.137](https://github.com/payloadcms/payload/commit/5c1e2846a2694a80cc8707703406c2ac1bb6af8a) (2020-11-12)
## [0.0.137](https://github.com/payloadcms/payload/commit/5c1e2846a2694a80cc8707703406c2ac1bb6af8a) (2020-11-12)

View File

@@ -16,7 +16,7 @@
</a>
<a href="https://discord.com/invite/r6sCXqVk3v">
<img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord" />
<img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord&color=7289da" />
</a>
</p>
@@ -68,6 +68,7 @@ Alternatively, it only takes about five minutes to [create an app from scratch](
### Documentation
Check out the [Payload website](https://payloadcms.com/docs/getting-started/what-is-payload) to find in-depth documentation for everything that Payload offers.
### Contributing
If you want to add contributions to this repository, please follow the instructions in [contributing.md](./contributing.md).

View File

@@ -1,12 +1,21 @@
export {
useForm,
/**
* @deprecated useWatchForm is no longer preferred. If you need all form fields, prefer `useAllFormFields`.
*/
useWatchForm,
useFormFields,
useAllFormFields,
useFormSubmitted,
useFormProcessing,
useFormModified,
} from '../dist/admin/components/forms/Form/context';
export { default as useField } from '../dist/admin/components/forms/useField';
/**
* @deprecated This method is now called useField. The useFieldType alias will be removed in an upcoming version.
*/
export { default as useFieldType } from '../dist/admin/components/forms/useField';
export { default as Form } from '../dist/admin/components/forms/Form';
@@ -24,5 +33,6 @@ export { default as Submit } from '../dist/admin/components/forms/Submit';
export { default as Label } from '../dist/admin/components/forms/Label';
export { default as reduceFieldsToValues } from '../dist/admin/components/forms/Form/reduceFieldsToValues';
export { default as getSiblingData } from '../dist/admin/components/forms/Form/getSiblingData';
export { default as withCondition } from '../dist/admin/components/forms/withCondition';

View File

@@ -3,3 +3,4 @@ export { useLocale } from '../dist/admin/components/utilities/Locale';
export { useDocumentInfo } from '../dist/admin/components/utilities/DocumentInfo';
export { useConfig } from '../dist/admin/components/utilities/Config';
export { useAuth } from '../dist/admin/components/utilities/Auth';
export { useEditDepth } from '../dist/admin/components/utilities/EditDepth';

View File

@@ -49,7 +49,9 @@ The directory split up in this way specifically to reduce friction when creating
The following command will start Payload with your config: `yarn dev my-test-dir`. This command will start up Payload using your config and refresh a test database on every restart.
NOTE: It is recommended to add the test credentials to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart.
When switching between test directories, you will want to remove your `node_modules/.cache ` manually or by running `yarn clean:cache`.
NOTE: It is recommended to add the test credentials (located in `test/credentials.ts`) to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart.
## Pull Requests

View File

@@ -8,6 +8,11 @@ keywords: overview, access control, permissions, documentation, Content Manageme
Access control within Payload is extremely powerful while remaining easy and intuitive to manage. Declaring who should have access to what documents is no more complex than writing a simple JavaScript function that either returns a `boolean` or a [`query`](/docs/queries/overview) constraint to restrict which documents users can interact with.
<YouTube
id="DoPLyXG26Dg"
title="Overview of Payload Access Control"
/>
**Example use cases:**
- Allowing anyone `read` access to all `Post`s

View File

@@ -11,35 +11,39 @@ While designing the Payload Admin panel, we determined it should be as minimal a
To swap in your own React component, first, consult the list of available component overrides below. Determine the scope that corresponds to what you are trying to accomplish, and then author your React component accordingly.
<Banner type="success">
<strong>Tip:</strong><br/>
Custom components will automatically be provided with all props that the default component would accept.
<strong>Tip:</strong>
<br />
Custom components will automatically be provided with all props that the
default component would accept.
</Banner>
### Base Component Overrides
You can override a set of admin panel-wide components by providing a component to your base Payload config's `admin.components` property. The following options are available:
| Path | Description |
| --------------------- | -------------|
| **`Nav`** | Contains the sidebar and mobile Nav in its entirety. |
| **`BeforeDashboard`** | Array of components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
| **`AfterDashboard`** | Array of components to inject into the built-in Dashboard, _after_ the default dashboard contents. [Demo](https://github.com/payloadcms/payload/tree/master/test/admin/components/AfterDashboard/index.tsx)|
| **`BeforeLogin`** | Array of components to inject into the built-in Login, _before_ the default login form. |
| **`AfterLogin`** | Array of components to inject into the built-in Login, _after_ the default login form. |
| **`BeforeNavLinks`** | Array of components to inject into the built-in Nav, _before_ the links themselves. |
| **`AfterNavLinks`** | Array of components to inject into the built-in Nav, _after_ the links. |
| **`views.Account`** | The Account view is used to show the currently logged in user's Account page. |
| **`views.Dashboard`** | The main landing page of the Admin panel. |
| **`graphics.Icon`** | Used as a graphic within the `Nav` component. Often represents a condensed version of a full logo. |
| **`graphics.Logo`** | The full logo to be used in contexts like the `Login` view. |
| **`routes`** | Define your own routes to add to the Payload Admin UI. [More](#custom-routes) |
| **`providers`** | Define your own provider components that will wrap the Payload Admin UI. [More](#custom-providers) |
| Path | Description |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`Nav`** | Contains the sidebar and mobile Nav in its entirety. |
| **`logout.Button`** | A custom React component.
| **`BeforeDashboard`** | Array of components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
| **`AfterDashboard`** | Array of components to inject into the built-in Dashboard, _after_ the default dashboard contents. [Demo](https://github.com/payloadcms/payload/tree/master/test/admin/components/AfterDashboard/index.tsx) |
| **`BeforeLogin`** | Array of components to inject into the built-in Login, _before_ the default login form. |
| **`AfterLogin`** | Array of components to inject into the built-in Login, _after_ the default login form. |
| **`BeforeNavLinks`** | Array of components to inject into the built-in Nav, _before_ the links themselves. |
| **`AfterNavLinks`** | Array of components to inject into the built-in Nav, _after_ the links. |
| **`views.Account`** | The Account view is used to show the currently logged in user's Account page. |
| **`views.Dashboard`** | The main landing page of the Admin panel. |
| **`graphics.Icon`** | Used as a graphic within the `Nav` component. Often represents a condensed version of a full logo. |
| **`graphics.Logo`** | The full logo to be used in contexts like the `Login` view. |
| **`routes`** | Define your own routes to add to the Payload Admin UI. [More](#custom-routes) |
| **`providers`** | Define your own provider components that will wrap the Payload Admin UI. [More](#custom-providers) |
#### Full example:
`payload.config.js`
```ts
import { buildConfig } from 'payload/config'
import { buildConfig } from "payload/config";
import {
MyCustomNav,
MyCustomLogo,
@@ -47,7 +51,7 @@ import {
MyCustomAccount,
MyCustomDashboard,
MyProvider,
} from './customComponents';
} from "./customComponents";
export default buildConfig({
admin: {
@@ -67,134 +71,136 @@ export default buildConfig({
});
```
*For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/master/test/admin/components).*
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/master/test/admin/components)._
### Collections
You can override components on a Collection-by-Collection basis via each Collection's `admin` property.
| Path | Description |
| ---------------- | -------------|
| **`views.Edit`** | Used while a document within this Collection is being edited. |
| **`views.List`** | The `List` view is used to render a paginated, filterable table of Documents in this Collection. |
| Path | Description |
| ---------------- | ------------------------------------------------------------------------------------------------ |
| **`views.Edit`** | Used while a document within this Collection is being edited. |
| **`views.List`** | The `List` view is used to render a paginated, filterable table of Documents in this Collection. |
### Globals
As with Collections, You can override components on a global-by-global basis via their `admin` property.
| Path | Description |
| ---------------- | -------------|
| **`views.Edit`** | Used while this Global is being edited. |
| Path | Description |
| ---------------- | --------------------------------------- |
| **`views.Edit`** | Used while this Global is being edited. |
### Fields
All Payload fields support the ability to swap in your own React components. So, for example, instead of rendering a default Text input, you might need to render a color picker that provides the editor with a custom color picker interface to restrict the data entered to colors only.
<Banner type="success">
<strong>Tip:</strong><br/>
Don't see a built-in field type that you need? Build it! Using a combination of custom validation and custom components, you can override the entirety of how a component functions within the admin panel and effectively create your own field type.
<strong>Tip:</strong>
<br />
Don't see a built-in field type that you need? Build it! Using a combination
of custom validation and custom components, you can override the entirety of
how a component functions within the admin panel and effectively create your
own field type.
</Banner>
**Fields support the following custom components:**
| Component | Description |
| --------------- | -------------|
| **`Filter`** | Override the text input that is presented in the `List` view when a user is filtering documents by the customized field. |
| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. |
| **`Field`** | Swap out the field itself within all `Edit` views. |
| Component | Description |
| ------------ | --------------------------------------------------------------------------------------------------------------------------- |
| **`Filter`** | Override the text input that is presented in the `List` view when a user is filtering documents by the customized field. |
| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. [More](#cell-component) |
| **`Field`** | Swap out the field itself within all `Edit` views. [More](#field-component) |
## Cell Component
These are the props that will be passed to your custom Cell to use in your own components.
| Property | Description |
| ---------------- | ----------------------------------------------------------------- |
| **`field`** | An object that includes the field configuration. |
| **`colIndex`** | A unique number for the column in the list. |
| **`collection`** | An object with the config of the collection that the field is in. |
| **`cellData`** | The data for the field that the cell represents. |
| **`rowData`** | An object with all the field values for the row. |
#### Example
```tsx
import React from "react";
import "./index.scss";
const baseClass = "custom-cell";
const CustomCell: React.FC<Props> = (props) => {
const { field, colIndex, collection, cellData, rowData } = props;
return <span className={baseClass}>{cellData}</span>;
};
```
## Field Component
When writing your own custom components you can make use of a number of hooks to set data, get reactive changes to other fields, get the id of the document or interact with a context from a custom provider.
### Sending and receiving values from the form
When swapping out the `Field` component, you'll be responsible for sending and receiving the field's `value` from the form itself. To do so, import the `useField` hook as follows:
```tsx
import { useField } from 'payload/components/forms'
import { useField } from "payload/components/forms";
type Props = { path: string }
type Props = { path: string };
const CustomTextField: React.FC<Props> = ({ path }) => {
// highlight-start
const { value, setValue } = useField<Props>({ path })
const { value, setValue } = useField<Props>({ path });
// highlight-end
return <input onChange={e => setValue(e.target.value)} value={value.path} />
}
```
### Getting other field values from the form
There are times when a custom field component needs to have access to data from other fields. This can be done using `getDataByPath` from `useWatchForm` as follows:
```tsx
import { useWatchForm } from 'payload/components/forms';
const DisplayFee: React.FC = () => {
const { getDataByPath } = useWatchForm();
const amount = getDataByPath('amount');
const feePercentage = getDataByPath('feePercentage');
if (amount && feePercentage) {
return (
<span>The fee is ${(amount * feePercentage) / 100}</span>
);
}
};
```
### Getting the document ID
The document ID can be very useful for certain custom components. You can get the `id` from the `useDocumentInfo` hook. Here is an example of a `UI` field using `id` to link to related collections:
```tsx
import { useDocumentInfo } from 'payload/components/utilities';
const LinkFromCategoryToPosts: React.FC = () => {
// highlight-start
const { id } = useDocumentInfo();
// highlight-end
// id will be undefined on the create form
if (!id) {
return null;
}
return (
<a href={`/admin/collections/posts?where[or][0][and][0][category][in][0]=[${id}]`} >
View posts
</a>
)
<input onChange={(e) => setValue(e.target.value)} value={value.path} />
);
};
```
<Banner type="success">
For more information regarding the hooks that are available to you while you
build custom components, including the <strong>useField</strong> hook,{" "}
<a href="/docs/admin/hooks" style={{ color: "black" }}>
click here
</a>
.
</Banner>
## Custom routes
You can easily add your own custom routes to the Payload Admin panel using the `admin.components.routes` property. Payload currently uses the extremely powerful React Router v5.x and custom routes support all the properties of the React Router `<Route />` component.
**Custom routes support the following properties:**
| Property | Description |
| ----------------- | -------------|
| **`Component`** * | Pass in the component that should be rendered when a user navigates to this route. |
| **`path`** * | React Router `path`. [See the React Router docs](https://v5.reactrouter.com/web/api/Route/path-string-string) for more info. |
| **`exact`** | React Router `exact` property. [More](https://v5.reactrouter.com/web/api/Route/exact-bool) |
| **`strict`** | React Router `strict` property. [More](https://v5.reactrouter.com/web/api/Route/strict-bool) |
| **`sensitive`** | React Router `sensitive` property. [More](https://v5.reactrouter.com/web/api/Route/sensitive-bool) |
| Property | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| **`Component`** \* | Pass in the component that should be rendered when a user navigates to this route. |
| **`path`** \* | React Router `path`. [See the React Router docs](https://v5.reactrouter.com/web/api/Route/path-string-string) for more info. |
| **`exact`** | React Router `exact` property. [More](https://v5.reactrouter.com/web/api/Route/exact-bool) |
| **`strict`** | React Router `strict` property. [More](https://v5.reactrouter.com/web/api/Route/strict-bool) |
| **`sensitive`** | React Router `sensitive` property. [More](https://v5.reactrouter.com/web/api/Route/sensitive-bool) |
*\* An asterisk denotes that a property is required.*
_\* An asterisk denotes that a property is required._
#### Custom route components
Your custom route components will be given all the props that a React Router `<Route />` typically would receive, as well as two props from Payload:
| Prop | Description |
| ---------------------- | -------------|
| **`user`** | The currently logged in user. Will be `null` if no user is logged in. |
| **`canAccessAdmin`** * | If the currently logged in user is allowed to access the admin panel or not. |
| Prop | Description |
| ----------------------- | ---------------------------------------------------------------------------- |
| **`user`** | The currently logged in user. Will be `null` if no user is logged in. |
| **`canAccessAdmin`** \* | If the currently logged in user is allowed to access the admin panel or not. |
<Banner type="warning">
<strong>Note:</strong><br/>
It's up to you to secure your custom routes. If your route requires a user to be logged in or to have certain access rights, you should handle that within your route component yourself.
<strong>Note:</strong>
<br />
It's up to you to secure your custom routes. If your route requires a user to
be logged in or to have certain access rights, you should handle that within
your route component yourself.
</Banner>
#### Example
@@ -210,7 +216,10 @@ To see how to pass in your custom views to create custom routes of your own, tak
As your admin customizations gets more complex you may want to share state between fields or other components. You can add custom providers to do add your own context to any Payload app for use in other custom components within the admin panel. Within your config add `admin.components.providers`, these can be used to share context or provide other custom functionality. Read the [React context](https://reactjs.org/docs/context.html) docs to learn more.
<Banner type="warning"><strong>Reminder:</strong> Don't forget to pass the **children** prop through the provider component for the admin UI to show</Banner>
<Banner type="warning">
<strong>Reminder:</strong> Don't forget to pass the **children** prop through
the provider component for the admin UI to show
</Banner>
### Styling Custom Components
@@ -227,7 +236,7 @@ To make use of Payload SCSS variables / mixins to use directly in your own compo
In any custom component you can get the selected locale with the `useLocale` hook. Here is a simple example:
```tsx
import { useLocale } from 'payload/components/utilities';
import { useLocale } from "payload/components/utilities";
const Greeting: React.FC = () => {
// highlight-start
@@ -235,12 +244,10 @@ const Greeting: React.FC = () => {
// highlight-end
const trans = {
en: 'Hello',
es: 'Hola',
en: "Hello",
es: "Hola",
};
return (
<span> { trans[locale] } </span>
);
return <span> {trans[locale]} </span>;
};
```

View File

@@ -1,7 +1,7 @@
---
title: Customizing CSS & SCSS
label: Customizing CSS
order: 30
order: 40
desc: Customize your Payload admin panel further by adding your own CSS or SCSS style sheet to the configuration, powerful theme and design options are waiting for you.
keywords: admin, css, scss, documentation, Content Management System, cms, headless, javascript, node, react, express
---

289
docs/admin/hooks.mdx Normal file
View File

@@ -0,0 +1,289 @@
---
title: React Hooks
label: React Hooks
order: 30
desc: Make use of all of the powerful React hooks that Payload provides.
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, express
---
Payload provides a variety of powerful hooks that can be used within your own React components. With them, you can interface with Payload itself and build just about any type of complex customization you can think of—directly in familiar React code.
### useField
The `useField` hook is used internally within every applicable Payload field component, and it manages sending and receiving a field's state from its parent form.
Outside of internal use, its most common use-case is in custom `Field` components. When you build a custom React `Field` component, you'll be responsible for sending and receiving the field's `value` from the form itself. To do so, import the `useField` hook as follows:
```tsx
import { useField } from 'payload/components/forms'
type Props = { path: string }
const CustomTextField: React.FC<Props> = ({ path }) => {
// highlight-start
const { value, setValue } = useField<string>({ path })
// highlight-end
return <input onChange={e => setValue(e.target.value)} value={value.path} />
}
```
The `useField` hook accepts an `args` object and sends back information and helpers for you to make use of:
```ts
const field = useField<string>({
path: 'fieldPathHere', // required
validate: myValidateFunc, // optional
disableFormData?: false, // if true, the field's data will be ignored
condition?: myConditionHere, // optional, used to skip validation if condition fails
})
// Here is what `useField` sends back
const {
showError, // whether or not the field should show as errored
errorMessage, // the error message to show, if showError
value, // the current value of the field from the form
formSubmitted, // if the form has been submitted
formProcessing, // if the form is currently processing
setValue, // method to set the field's value in form state
initialValue, // the initial value that the field mounted with
} = field;
// The rest of your component goes here
```
### useFormFields
There are times when a custom field component needs to have access to data from other fields, and you have a few options to do so. The `useFormFields` hook is a powerful and highly performant way to retrieve a form's field state, as well as to retrieve the `dispatchFields` method, which can be helpful for setting other fields' form states from anywhere within a form.
<Banner type="success">
<strong>This hook is great for retrieving only certain fields from form state</strong> because it ensures that it will only cause a rerender when the items that you ask for change.
</Banner>
Thanks to the awesome package [`use-context-selector`](https://github.com/dai-shi/use-context-selector), you can retrieve a specific field's state easily. This is ideal because you can ensure you have an up-to-date field state, and your component will only re-render when _that field's state_ changes.
You can pass a Redux-like selector into the hook, which will ensure that you retrieve only the field that you want. The selector takes an argument with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
```tsx
import { useFormFields } from 'payload/components/forms';
const MyComponent: React.FC = () => {
// Get only the `amount` field state, and only cause a rerender when that field changes
const amount = useFormFields(([fields, dispatch]) => fields.amount);
// Do the same thing as above, but to the `feePercentage` field
const feePercentage = useFormFields(([fields, dispatch]) => fields.feePercentage);
if (typeof amount?.value !== 'undefined' && typeof feePercentage?.value !== 'undefined') {
return (
<span>The fee is ${(amount.value * feePercentage.value) / 100}</span>
);
}
};
```
### useAllFormFields
**To retrieve more than one field**, you can use the `useAllFormFields` hook. Your component will re-render when _any_ field changes, so use this hook only if you absolutely need to. Unlike the `useFormFields` hook, this hook does not accept a "selector", and it always returns an array with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
You can do lots of powerful stuff by retrieving the full form state, like using built-in helper functions to reduce field state to values only, or to retrieve sibling data by path.
```tsx
import { useAllFormFields, reduceFieldsToValues, getSiblingData } from 'payload/components/forms';
const ExampleComponent: React.FC = () => {
// the `fields` const will be equal to all fields' state,
// and the `dispatchFields` method is usable to send field state up to the form
const [fields, dispatchFields] = useAllFormFields();
// Pass in fields, and indicate if you'd like to "unflatten" field data.
// The result below will reflect the data stored in the form at the given time
const formData = reduceFieldsToValues(fields, true);
// Pass in field state and a path,
// and you will be sent all sibling data of the path that you've specified
const siblingData = getSiblingData(fields, 'someFieldName');
return (
// return some JSX here if necessary
)
};
```
##### Updating other fields' values
If you are building a custom component, then you should use `setValue` which is returned from the `useField` hook to programmatically set your field's value. But if you're looking to update _another_ field's value, you can use `dispatchFields` returned from `useFormFields`.
You can send the following actions to the `dispatchFields` function.
| Action | Description |
|------------------------|----------------------------------------------------------------------------|
| **`ADD_ROW`** | Adds a row of data (useful in array / block field data) |
| **`DUPLICATE_ROW`** | Duplicates a row of data (useful in array / block field data) |
| **`MODIFY_CONDITION`** | Updates a field's conditional logic result (true / false) |
| **`MOVE_ROW`** | Moves a row of data (useful in array / block field data) |
| **`REMOVE`** | Removes a field from form state |
| **`REMOVE_ROW`** | Removes a row of data from form state (useful in array / block field data) |
| **`REPLACE_STATE`** | Completely replaces form state |
| **`UPDATE`** | Update any property of a specific field's state |
To see types for each action supported within the `dispatchFields` hook, check out the Form types [here](https://github.com/payloadcms/payload/blob/master/src/admin/components/forms/Form/types.ts).
### useForm
The `useForm` hook can be used to interact with the form itself, and sends back many methods that can be used to reactively fetch form state without causing rerenders within your components each time a field is changed. This is useful if you have action-based callbacks that your components fire, and need to interact with form state _based on a user action_.
<Banner type="warning">
<strong>Warning:</strong><br/>
This hook is optimized to avoid causing rerenders when fields change, and as such, its `fields` property will be out of date. You should only leverage this hook if you need to perform actions against the form in response to your users' actions. Do not rely on its returned "fields" as being up-to-date. They will be removed from this hook's response in an upcoming version.
</Banner>
The `useForm` hook returns an object with the following properties:
| Action | Description |
|----------------------|---------------------------------------------------------------------|
| **`fields`** | Deprecated. This property cannot be relied on as up-to-date. |
| **`submit`** | Method to trigger the form to submit |
| **`dispatchFields`** | Dispatch actions to the form field state |
| **`validateForm`** | Trigger a validation of the form state |
| **`createFormData`** | Create a `multipart/form-data` object from the current form's state |
| **`disabled`** | Boolean denoting whether or not the form is disabled |
| **`getFields`** | Gets all fields from state |
| **`getField`** | Gets a single field from state by path |
| **`getData`** | Returns the data stored in the form |
| **`getSiblingData`** | Returns form sibling data for the given field path |
| **`setModified`** | Set the form's `modified` state |
| **`setProcessing`** | Set the form's `processing` state |
| **`setSubmitted`** | Set the form's `submitted` state |
| **`formRef`** | The ref from the form HTML element |
| **`reset`** | Method to reset the form to its initial state |
### useDocumentInfo
The `useDocumentInfo` hook provides lots of information about the document currently being edited, including the following:
| Property | Description |
|---------------------------|------------------------------------------------------------------------------------|
| **`collection`** | If the doc is a collection, its collection config will be returned |
| **`global`** | If the doc is a global, its global config will be returned |
| **`type`** | The type of document being edited (collection or global) |
| **`id`** | If the doc is a collection, its ID will be returned |
| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences |
| **`versions`** | Versions of the current doc |
| **`unpublishedVersions`** | Unpublished versions of the current doc |
| **`publishedDoc`** | The currently published version of the doc being edited |
| **`getVersions`** | Method to trigger the retrieval of document versions |
**Example:**
```tsx
import { useDocumentInfo } from 'payload/components/utilities';
const LinkFromCategoryToPosts: React.FC = () => {
// highlight-start
const { id } = useDocumentInfo();
// highlight-end
// id will be undefined on the create form
if (!id) {
return null;
}
return (
<a href={`/admin/collections/posts?where[or][0][and][0][category][in][0]=[${id}]`} >
View posts
</a>
)
};
```
### useLocale
In any custom component you can get the selected locale with the `useLocale` hook. Here is a simple example:
```tsx
import { useLocale } from 'payload/components/utilities';
const Greeting: React.FC = () => {
// highlight-start
const locale = useLocale();
// highlight-end
const trans = {
en: 'Hello',
es: 'Hola',
};
return (
<span> { trans[locale] } </span>
);
};
```
### useAuth
Useful to retrieve info about the currently logged in user as well as methods for interacting with it. It sends back an object with the following properties:
| Property | Description |
|---------------------|-----------------------------------------------------------------------------------------|
| **`user`** | The currently logged in user |
| **`logOut`** | A method to log out the currently logged in user |
| **`refreshCookie`** | A method to trigger the silent refreshing of a user's auth token |
| **`setToken`** | Set the token of the user, to be decoded and used to reset the user and token in memory |
| **`token`** | The logged in user's token (useful for creating preview links, etc.) |
| **`permissions`** | The permissions of the current user |
```tsx
import { useAuth } from 'payload/components/utilities';
import { User } from '../payload-types.ts';
const Greeting: React.FC = () => {
// highlight-start
const { user } = useConfig<User>();
// highlight-end
return (
<span>Hi, {user.email}!</span>
);
};
```
### useConfig
Used to easily fetch the full Payload config.
```tsx
import { useConfig } from 'payload/components/utilities';
const MyComponent: React.FC = () => {
// highlight-start
const config = useConfig();
// highlight-end
return (
<span>{config.serverURL}</span>
);
};
```
### useEditDepth
Sends back how many editing levels "deep" the current component is. Edit depth is relevant while adding new documents / editing documents in modal windows and other cases.
```tsx
import { useEditDepth } from 'payload/components/utilities';
const MyComponent: React.FC = () => {
// highlight-start
const editDepth = useEditDepth();
// highlight-end
return (
<span>My component is {editDepth} levels deep</span>
)
}
```
### usePreferences
Returns methods to set and get user preferences. More info can be found [here](https://payloadcms.com/docs/admin/preferences).

View File

@@ -14,7 +14,7 @@ The Payload Admin panel is built with Webpack, code-split, highly performant (ev
The Admin panel is meant to be simple enough to give you a starting point but not bring too much complexity, so that you can easily customize it to suit the needs of your application and your editors.
</Banner>
![Payload's Admin panel built in React](https://payloadcms.com/images/admin.jpg)
![Payload's Admin panel built in React](https://payloadcms.com/images/docs/admin.jpg)
*Screenshot of the Admin panel while editing a document from an example `AllFields` collection*
@@ -22,18 +22,20 @@ The Payload Admin panel is built with Webpack, code-split, highly performant (ev
All options for the Admin panel are defined in your base Payload config file.
| Option | Description |
| -------------------- | -------------|
| `user` | The `slug` of a Collection that you want be used to log in to the Admin dashboard. [More](/docs/admin/overview#the-admin-user-collection) |
| `meta` | Base meta data to use for the Admin panel. Included properties are `titleSuffix`, `ogImage`, and `favicon`. |
| `disable` | If set to `true`, the entire Admin panel will be disabled. |
| `indexHTML` | Optionally replace the entirety of the `index.html` file used by the Admin panel. Reference the [base index.html file](https://github.com/payloadcms/payload/blob/master/src/admin/index.html) to ensure your replacement has the appropriate HTML elements. |
| `css` | Absolute path to a stylesheet that you can use to override / customize the Admin panel styling. [More](/docs/admin/customizing-css). |
| `scss` | Absolute path to a Sass variables / mixins stylesheet meant to override Payload styles to make for an easy re-skinning of the Admin panel. [More](/docs/admin/customizing-css#overriding-scss-variables). |
| `dateFormat` | Global date format that will be used for all dates in the Admin panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
| `components` | Component overrides that affect the entirety of the Admin panel. [More](/docs/admin/components) |
| `webpack` | Customize the Webpack config that's used to generate the Admin panel. [More](/docs/admin/webpack) |
| Option | Description |
| --------------------- | -------------|
| `user` | The `slug` of a Collection that you want be used to log in to the Admin dashboard. [More](/docs/admin/overview#the-admin-user-collection) |
| `meta` | Base meta data to use for the Admin panel. Included properties are `titleSuffix`, `ogImage`, and `favicon`. |
| `disable` | If set to `true`, the entire Admin panel will be disabled. |
| `indexHTML` | Optionally replace the entirety of the `index.html` file used by the Admin panel. Reference the [base index.html file](https://github.com/payloadcms/payload/blob/master/src/admin/index.html) to ensure your replacement has the appropriate HTML elements. |
| `css` | Absolute path to a stylesheet that you can use to override / customize the Admin panel styling. [More](/docs/admin/customizing-css). |
| `scss` | Absolute path to a Sass variables / mixins stylesheet meant to override Payload styles to make for an easy re-skinning of the Admin panel. [More](/docs/admin/customizing-css#overriding-scss-variables). |
| `dateFormat` | Global date format that will be used for all dates in the Admin panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
| `components` | Component overrides that affect the entirety of the Admin panel. [More](/docs/admin/components) |
| `webpack` | Customize the Webpack config that's used to generate the Admin panel. [More](/docs/admin/webpack) | |
| **`logoutRoute`** | The route for the `logout` page. |
| **`inactivityRoute`** | The route for the `logout` inactivity page. |
### The Admin User Collection

View File

@@ -1,7 +1,7 @@
---
title: Managing User Preferences
label: Preferences
order: 40
order: 50
desc: Store the preferences of your users as they interact with the Admin panel.
keywords: admin, preferences, custom, customize, documentation, Content Management System, cms, headless, javascript, node, react, express
---

View File

@@ -1,7 +1,7 @@
---
title: Webpack
label: Webpack
order: 50
order: 60
desc: The Payload admin panel uses Webpack 5 and supports many common functionalities such as SCSS and Typescript out of the box to give you more freedom.
keywords: admin, webpack, documentation, Content Management System, cms, headless, javascript, node, react, express
---
@@ -155,6 +155,11 @@ export default {};
Now, when Webpack sees that you're attempting to import your `createStripeSubscriptionPath` file, it'll disregard that actual file and load your mock file instead. Not only will your Admin panel now bundle successfully, you will have optimized its filesize by removing unnecessary code! And you might have learned something about Webpack, too.
<Banner type="success">
<strong>Tip:</strong><br/>
If changes to your Webpack aliases are not surfacing, they might be [cached](https://webpack.js.org/configuration/cache/) in `node_modules/.cache/webpack`. Try deleting that folder and restarting your server.
</Banner>
## Admin environment vars
<Banner type="warning">

View File

@@ -57,6 +57,8 @@ const response = await fetch("http://localhost:3000/api/pages", {
});
```
Note: The label portion of the header is case-sensitive and will likely have a capitalized first character unless the label has been customized.
### Forgot Password
You can customize how the Forgot Password workflow operates with the following options on the `auth.forgotPassword` property:

View File

@@ -12,7 +12,7 @@ keywords: authentication, config, configuration, overview, documentation, Conten
Authentication is used within the Payload Admin panel itself as well as throughout your app(s) themselves however you determine necessary.
![Authentication admin panel functionality](https://payloadcms.com/images/auth-admin.jpg)
![Authentication admin panel functionality](https://payloadcms.com/images/docs/auth-admin.jpg)
*Admin panel screenshot depicting an Admins Collection with Auth enabled*
**Here are some common use cases of Authentication outside of Payload's dashboard itself:**

View File

@@ -59,17 +59,18 @@ You can find an assortment of [example collection configs](https://github.com/pa
You can customize the way that the Admin panel behaves on a collection-by-collection basis by defining the `admin` property on a collection's config.
| Option | Description |
| ---------------------------- | -------------|
| `group` | Text used as a label for grouping collection links together in the navigation. |
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin panel. If no field is defined, the ID of the document is used as the title. |
| `description` | Text or React component to display below the Collection label in the List view to give editors more information. |
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this collection's List view. |
| `disableDuplicate ` | Disables the "Duplicate" button while editing documents within this collection. |
| Option | Description |
| --------------------------- | -------------|
| `group` | Text used as a label for grouping collection links together in the navigation. |
| `hooks` | Admin-specific hooks for this collection. [More](#admin-hooks) |
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin panel. If no field is defined, the ID of the document is used as the title. |
| `description` | Text or React component to display below the Collection label in the List view to give editors more information. |
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this collection's List view. |
| `disableDuplicate ` | Disables the "Duplicate" button while editing documents within this collection. |
| `enableRichTextRelationship` | The [Rich Text](/docs/fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `preview` | Function to generate preview URLS within the Admin panel that can point to your app. [More](#preview). |
| `components` | Swap in your own React components to be used within this collection. [More](/docs/admin/components#collections) |
| `listSearchableFields ` | Specify which fields should be searched in the List search view. [More](/docs/configuration/collections#list-searchable-fields) |
| `preview` | Function to generate preview URLS within the Admin panel that can point to your app. [More](#preview). |
| `components` | Swap in your own React components to be used within this collection. [More](/docs/admin/components#collections) |
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More](#list-searchable-fields) |
### Preview
@@ -120,7 +121,7 @@ Hooks are a powerful way to extend collection functionality and execute your own
Collections support all field types that Payload has to offer—including simple fields like text and checkboxes all the way to more complicated layout-building field groups like Blocks. [Click here](/docs/fields/overview) to learn more about field types.
#### List Searchable Fields
### List Searchable Fields
In the List view, there is a "search" box that allows you to quickly find a document with a search. By default, it searches on the ID field. If you have `admin.useAsTitle` defined, the list search will use that field. However, you can define more than one field to search to make it easier on your admin editors to find the data they need.
@@ -128,9 +129,53 @@ For example, let's say you have a Posts collection with `title`, `metaDescriptio
<Banner type="warning">
<strong>Note:</strong><br/>
If you are adding <strong>listSearchableFields</strong>, make sure you index each of these fields so your admin queries can remain performant.
If you are adding <strong>listSearchableFields</strong>, make sure you index each of these fields so your admin queries can remain performant.
</Banner>
### Admin Hooks
In addition to collection hooks themselves, Payload provides for admin UI-specific hooks that you can leverage.
**`beforeDuplicate`**
The `beforeDuplicate` hook is an async function that accepts an object containing the data to duplicate, as well as the locale of the doc to duplicate. Within this hook, you can modify the data to be duplicated, which is useful in cases where you have unique fields that need to be incremented or similar, as well as if you want to automatically modify a document's `title`.
Example:
```ts
import { BeforeDuplicate, CollectionConfig } from 'payload/types';
// Your auto-generated Page type
import { Page } from '../payload-types.ts';
const beforeDuplicate: BeforeDuplicate<Page> = ({ data }) => {
return {
...data,
title: `${data.title} Copy`,
uniqueField: data.uniqueField ? `${data.uniqueField}-copy` : '',
};
};
export const Page: CollectionConfig = {
slug: 'pages',
admin: {
hooks: {
beforeDuplicate,
}
},
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'uniqueField',
type: 'text',
unique: true,
}
]
}
```
### TypeScript
You can import collection types as follows:

View File

@@ -8,7 +8,7 @@ keywords: overview, config, configuration, documentation, Content Management Sys
Payload is a *config-based*, code-first CMS and application framework. The Payload config is central to everything that Payload does. It scaffolds the data that Payload stores as well as maintains custom React components, hook logic, custom validations, and much more. The config itself and all of its dependencies are run through Babel, so you can take full advantage of newer JavaScript features and even directly import React components containing JSX.
<strong>Also, because the Payload source code is fully written in TypeScript, its configs are strongly typed—meaning that even if you aren't using TypeScript to build your project, your IDE (such as VSCode) may still provide helpful information like type-ahead suggestions while you write your config.</strong>
**Also, because the Payload source code is fully written in TypeScript, its configs are strongly typed—meaning that even if you aren't using TypeScript to build your project, your IDE (such as VSCode) may still provide helpful information like type-ahead suggestions while you write your config.**
<Banner type="warning">
<strong>Important:</strong><br />This file is included in the Payload admin bundle, so make sure you do not embed any sensitive information.

View File

@@ -16,7 +16,7 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
- Navigational structures where editors can specify nav items containing pages ([relationship field](/docs/fields/relationship)), an "open in new tab" [checkbox field](/docs/fields/checkbox)
- Event agenda "timeslots" where you need to specify start & end time ([date field](/docs/fields/date)), label ([text field](/docs/fields/text)), and Learn More page [relationship](/docs/fields/relationship)
![Array field in Payload admin panel](https://payloadcms.com/images/fields/array.jpg)
![Array field in Payload admin panel](https://payloadcms.com/images/docs/fields/array.jpg)
*Admin panel screenshot of an Array field with a Row containing two text fields, a read-only text field and a checkbox*
### Config

View File

@@ -16,7 +16,7 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme
- A form builder tool where available block configs might be `Text`, `Select`, or `Checkbox`.
- Virtual event agenda "timeslots" where a timeslot could either be a `Break`, a `Presentation`, or a `BreakoutSession`.
![Blocks field in Payload admin panel](https://payloadcms.com/images/fields/blocks.jpg)
![Blocks field in Payload admin panel](https://payloadcms.com/images/docs/fields/blocks.jpg)
*Admin panel screenshot of a Blocks field type with Call to Action and Number block examples*

View File

@@ -23,7 +23,7 @@ keywords: radio, fields, config, configuration, documentation, Content Managemen
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
@@ -41,7 +41,7 @@ In addition to the default [field admin config](/docs/fields/overview#admin-conf
**`layout`**
The `layout` property allows for the radio group to be styled as a horizonally or vertically distributed list.
The `layout` property allows for the radio group to be styled as a horizonally or vertically distributed list. The default value is `horizontal`.
### Example
@@ -65,7 +65,7 @@ const ExampleCollection: CollectionConfig = {
value: 'dark_gray',
},
],
defaultValue: 'option_1',
defaultValue: 'mint', // The first value in options.
admin: {
layout: 'horizontal',
}

View File

@@ -81,17 +81,17 @@ Set this property to `true` to hide this field's gutter within the admin panel.
This allows [fields](/docs/fields/overview) to be saved as extra fields on a link inside the Rich Text Editor. When this is present, the fields will render inside a modal that can be opened by clicking the "edit" button on the link element.
![RichText link fields](https://payloadcms.com/images/fields/richText/rte-link-fields-modal.jpg)
![RichText link fields](https://payloadcms.com/images/docs/fields/richText/rte-link-fields-modal.jpg)
*RichText link with custom fields*
**`upload.collections[collection-name].fields`**
This allows [fields](/docs/fields/overview) to be saved as meta data on an upload field inside the Rich Text Editor. When this is present, the fields will render inside a modal that can be opened by clicking the "edit" button on the upload element.
![RichText upload element](https://payloadcms.com/images/fields/richText/rte-upload-element.jpg)
![RichText upload element](https://payloadcms.com/images/docs/fields/richText/rte-upload-element.jpg)
*RichText field using the upload element*
![RichText upload element modal](https://payloadcms.com/images/fields/richText/rte-upload-fields-modal.jpg)
![RichText upload element modal](https://payloadcms.com/images/docs/fields/richText/rte-upload-fields-modal.jpg)
*RichText upload element modal displaying fields from the config*
### Relationship element
@@ -174,6 +174,20 @@ const ExampleCollection: CollectionConfig = {
]
}
],
link: {
// Inject your own fields into the Link element
fields: [
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: [
'noopener', 'noreferrer', 'nofollow',
],
},
],
},
upload: {
collections: {
media: {

View File

@@ -10,7 +10,7 @@ keywords: tabs, fields, config, configuration, documentation, Content Management
The Tabs field is presentational-only and only affects the Admin panel (unless a tab is named). By using it, you can place fields within a nice layout component that separates certain sub-fields by a tabbed interface.
</Banner>
![Tabs field type used to separate Hero fields from Page Layout](https://payloadcms.com/images/fields/tabs/tabs.jpg)
![Tabs field type used to separate Hero fields from Page Layout](https://payloadcms.com/images/docs/fields/tabs/tabs.jpg)
*Tabs field type used to separate Hero fields from Page Layout*
### Config

View File

@@ -23,12 +23,12 @@ With this field, you can also inject custom `Cell` components that appear as add
### Config
| Option | Description |
| ---------------------------- | ----------- |
| **`name`** * | A unique identifier for this field. |
| **`label`** | Human-readable label for this UI field. |
| **`admin.components.Field`** | React component to be rendered for this field within the Edit view. |
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. |
| Option | Description |
| ---------------------------- |-------------------------------------------------------------------------------------------------------------------|
| **`name`** * | A unique identifier for this field. |
| **`label`** | Human-readable label for this UI field. |
| **`admin.components.Field`** | React component to be rendered for this field within the Edit view. [More](/docs/admin/components/#field-component) |
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More](/docs/admin/components/#field-component) |
*\* An asterisk denotes that a property is required.*

View File

@@ -48,7 +48,7 @@ The team behind Payload has been building websites and apps with existing conten
- Secure
- Fully flexible and extensible
Payload is our silver bullet solution. It represents over two years of passionate development and brings everything we need when we build new apps and websites:
Payload is our silver bullet solution. We've blended the best parts of our experience with other CMS and app frameworks into Payload, and we finally have everything we need when we build new apps and websites:
- A beautiful, dynamic, customizable admin UI
- Extensible and reusable authentication

View File

@@ -14,7 +14,7 @@ The labels you provide for your Collections and Globals are used to name the Gra
## GraphQL Options
At the top of your Payload config you can define all the options to manage GraphQL. The
At the top of your Payload config you can define all the options to manage GraphQL.
| Option | Description |
| -------------------- | -------------|

View File

@@ -190,7 +190,7 @@ const afterDeleteHook: CollectionAfterDeleteHook = async ({
### beforeLogin
For auth-enabled Collections, this hook runs after successful `login` operations. You can optionally modify the user that is returned.
For auth-enabled Collections, this hook runs during `login` operations where a user with the provided credentials exist, but before a token is generated and added to the response. You can optionally modify the user that is returned, or throw an error in order to deny the login operation.
```ts
import { CollectionBeforeLoginHook } from 'payload/types';
@@ -198,7 +198,6 @@ import { CollectionBeforeLoginHook } from 'payload/types';
const beforeLoginHook: CollectionBeforeLoginHook = async ({
req, // full express request
user, // user being logged in
token, // user token
}) => {
return user;
}
@@ -213,6 +212,8 @@ import { CollectionAfterLoginHook } from 'payload/types';
const afterLoginHook: CollectionAfterLoginHook = async ({
req, // full express request
user, // user that was logged in
token, // user token
}) => {...}
```

View File

@@ -27,9 +27,9 @@ Field-level hooks offer incredible potential for encapsulating your logic. They
Example field configuration:
```ts
import { CollectionConfig } from 'payload/types';
import { Field } from 'payload/types';
const ExampleCollection: CollectionConfig = {
const ExampleField: Field = {
name: 'name',
type: 'text',
// highlight-start

View File

@@ -63,8 +63,8 @@ You can specify more options within the Local API vs. REST or GraphQL due to the
| `depth` | [Control auto-population](/docs/getting-started/concepts#depth) of nested relationship and upload fields. |
| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. |
| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. |
| `overrideAccess` | Skip access control. By default, this property is set to false. |
| `user` | If you re-enable access control, you can specify a user to use against the access control checks. |
| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. |
| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. |
| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. |
| `pagination` | Set to false to return all documents and avoid querying for document counts. |
@@ -323,7 +323,7 @@ const result = await payload.updateGlobal({
## TypeScript
Local API calls also support passing in a generic. This is especially useful if you generate your TS types using a [generate types script](/docs/typescript/generate-types).
Local API calls also support passing in a generic. This is especially useful if you generate your TS types using a [generate types script](/docs/typescript/generating-types).
Here is an example of usage:

View File

@@ -27,7 +27,7 @@ Writing plugins is no more complex than writing regular JavaScript. If you know
### How to install plugins
The base Payload config allows for a `plugins` property which takes an `array` of [`Plugin`s](https://github.com/payloadcms/payload/blob/master/src/config/types.ts#L21).
The base Payload config allows for a `plugins` property which takes an `array` of [`Plugins`](https://github.com/payloadcms/payload/blob/master/src/config/types.ts#L21).
```js
import { buildConfig } from 'payload/config';
@@ -134,7 +134,7 @@ const addLastModified: Plugin = (incomingConfig: Config): Config => {
export default addLastModified;
```
#### Available Plugins
### Available Plugins
You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin).

View File

@@ -22,15 +22,17 @@ All Payload API routes are mounted prefixed to your config's `routes.api` URL se
Each collection is mounted using its `slug` value. For example, if a collection's slug is `users`, all corresponding routes will be mounted on `/api/users`.
Note: Collection slugs must be formatted in kebab-case
**All CRUD operations are exposed as follows:**
| Method | Path | Description |
| -------- | --------------------------- | -------------------------------------- |
| `GET` | `/api/{collectionSlug}` | Find paginated documents |
| `GET` | `/api/{collectionSlug}/:id` | Find a specific document by ID |
| `POST` | `/api/{collectionSlug}` | Create a new document |
| `PATCH` | `/api/{collectionSlug}/:id` | Update a document by ID |
| `DELETE` | `/api/{collectionSlug}/:id` | Delete an existing document by ID |
| `GET` | `/api/{collection-slug}` | Find paginated documents |
| `GET` | `/api/{collection-slug}/:id` | Find a specific document by ID |
| `POST` | `/api/{collection-slug}` | Create a new document |
| `PATCH` | `/api/{collection-slug}/:id` | Update a document by ID |
| `DELETE` | `/api/{collection-slug}/:id` | Delete an existing document by ID |
##### Additional `find` query parameters
@@ -47,14 +49,14 @@ Auth enabled collections are also given the following endpoints:
| Method | Path | Description |
| -------- | --------------------------- | ----------- |
| `POST` | `/api/{collectionSlug}/verify/:token` | [Email verification](/docs/authentication/operations#verify-by-email), if enabled. |
| `POST` | `/api/{collectionSlug}/unlock` | [Unlock a user's account](/docs/authentication/operations#unlock), if enabled. |
| `POST` | `/api/{collectionSlug}/login` | [Logs in](/docs/authentication/operations#login) a user with email / password. |
| `POST` | `/api/{collectionSlug}/logout` | [Logs out](/docs/authentication/operations#logout) a user. |
| `POST` | `/api/{collectionSlug}/refresh-token` | [Refreshes a token](/docs/authentication/operations#refresh) that has not yet expired. |
| `GET` | `/api/{collectionSlug}/me` | [Returns the currently logged in user with token](/docs/authentication/operations#me). |
| `POST` | `/api/{collectionSlug}/forgot-password` | [Password reset workflow](/docs/authentication/operations#forgot-password) entry point. |
| `POST` | `/api/{collectionSlug}/reset-password` | [To reset the user's password](/docs/authentication/operations#reset-password). |
| `POST` | `/api/{collection-slug}/verify/:token` | [Email verification](/docs/authentication/operations#verify-by-email), if enabled. |
| `POST` | `/api/{collection-slug}/unlock` | [Unlock a user's account](/docs/authentication/operations#unlock), if enabled. |
| `POST` | `/api/{collection-slug}/login` | [Logs in](/docs/authentication/operations#login) a user with email / password. |
| `POST` | `/api/{collection-slug}/logout` | [Logs out](/docs/authentication/operations#logout) a user. |
| `POST` | `/api/{collection-slug}/refresh-token` | [Refreshes a token](/docs/authentication/operations#refresh) that has not yet expired. |
| `GET` | `/api/{collection-slug}/me` | [Returns the currently logged in user with token](/docs/authentication/operations#me). |
| `POST` | `/api/{collection-slug}/forgot-password` | [Password reset workflow](/docs/authentication/operations#forgot-password) entry point. |
| `POST` | `/api/{collection-slug}/reset-password` | [To reset the user's password](/docs/authentication/operations#reset-password). |
## Globals
@@ -86,6 +88,7 @@ Each endpoint object needs to have:
| **`path`** | A string for the endpoint route after the collection or globals slug |
| **`method`** | The lowercase HTTP verb to use: 'get', 'head', 'post', 'put', 'delete', 'connect' or 'options' |
| **`handler`** | A function or array of functions to be called with **req**, **res** and **next** arguments. [Express](https://expressjs.com/en/guide/routing.html#route-handlers) |
| **`root`** | When `true`, defines the endpoint on the root Express app, bypassing Payload handlers and the `routes.api` subpath. Note: this only applies to top-level endpoints of your Payload config, endpoints defined on `collections` or `globals` cannot be root. |
Example:

View File

@@ -12,7 +12,7 @@ keywords: uploads, images, media, overview, documentation, Content Management Sy
control.
</Banner>
![Upload admin panel functionality](https://payloadcms.com/images/upload-admin.jpg)
![Upload admin panel functionality](https://payloadcms.com/images/docs/upload-admin.jpg)
_Admin panel screenshot depicting a Media Collection with Upload enabled_
**Here are some common use cases of Uploads:**

View File

@@ -1,8 +1,12 @@
{
"name": "payload",
"version": "1.1.3",
"version": "1.1.25",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"engines": {
"node": ">=14",
"yarn": ">=1.22 <2"
},
"author": {
"email": "info@payloadcms.com",
"name": "Payload CMS",
@@ -41,6 +45,7 @@
"test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed",
"test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test",
"test:components": "cross-env jest --config=jest.components.config.js",
"clean:cache": "rimraf node_modules/.cache",
"clean": "rimraf dist",
"release": "release-it",
"release:patch": "release-it patch",
@@ -85,7 +90,7 @@
"@babel/preset-typescript": "^7.12.1",
"@babel/register": "^7.11.5",
"@date-io/date-fns": "^2.10.6",
"@faceless-ui/modal": "^2.0.0-alpha.4",
"@faceless-ui/modal": "^2.0.1",
"@faceless-ui/scroll-info": "^1.2.3",
"@faceless-ui/window-info": "^2.0.2",
"@types/is-plain-object": "^2.0.4",
@@ -169,9 +174,9 @@
"react-sortable-hoc": "^2.0.0",
"react-toastify": "^8.2.0",
"sanitize-filename": "^1.6.3",
"sass": "^1.52.1",
"sass": "^1.55.0",
"sass-loader": "^12.6.0",
"sharp": "^0.29.3",
"sharp": "^0.31.1",
"slate": "^0.72.8",
"slate-history": "^0.66.0",
"slate-hyperscript": "^0.66.0",
@@ -180,6 +185,7 @@
"terser-webpack-plugin": "^5.0.3",
"ts-essentials": "^7.0.1",
"url-loader": "^4.1.1",
"use-context-selector": "^1.4.1",
"uuid": "^8.1.0",
"webpack": "^5.6.0",
"webpack-bundle-analyzer": "^4.4.1",
@@ -189,7 +195,7 @@
},
"devDependencies": {
"@playwright/test": "^1.23.1",
"@release-it/conventional-changelog": "^2.0.0",
"@release-it/conventional-changelog": "^5.1.1",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^13.0.1",
"@trbl/eslint-config": "^1.2.4",
@@ -274,7 +280,7 @@
"mongodb-memory-server": "^7.2.0",
"nodemon": "^2.0.6",
"passport-strategy": "^1.0.0",
"release-it": "^14.2.2",
"release-it": "^15.5.0",
"rimraf": "^3.0.2",
"serve-static": "^1.14.2",
"shelljs": "^0.8.5",

View File

@@ -3,15 +3,16 @@ import qs from 'qs';
export const requests = {
get: (url: string, params: unknown = {}): Promise<Response> => {
const query = qs.stringify(params, { addQueryPrefix: true });
return fetch(`${url}${query}`);
return fetch(`${url}${query}`, { credentials: 'include' });
},
post: (url: string, options: RequestInit = { headers: {} }): Promise<Response> => {
const headers = options && options.headers ? { ...options.headers } : {};
const formattedOptions = {
const formattedOptions: RequestInit = {
...options,
method: 'post',
credentials: 'include',
headers: {
...headers,
},
@@ -23,9 +24,10 @@ export const requests = {
put: (url: string, options: RequestInit = { headers: {} }): Promise<Response> => {
const headers = options && options.headers ? { ...options.headers } : {};
const formattedOptions = {
const formattedOptions: RequestInit = {
...options,
method: 'put',
credentials: 'include',
headers: {
...headers,
},
@@ -37,9 +39,10 @@ export const requests = {
patch: (url: string, options: RequestInit = { headers: {} }): Promise<Response> => {
const headers = options && options.headers ? { ...options.headers } : {};
const formattedOptions = {
const formattedOptions: RequestInit = {
...options,
method: 'PATCH',
credentials: 'include',
headers: {
...headers,
},
@@ -50,12 +53,16 @@ export const requests = {
delete: (url: string, options: RequestInit = { headers: {} }): Promise<Response> => {
const headers = options && options.headers ? { ...options.headers } : {};
return fetch(url, {
const formattedOptions: RequestInit = {
...options,
method: 'delete',
credentials: 'include',
headers: {
...headers,
},
});
};
return fetch(url, formattedOptions);
},
};

View File

@@ -32,9 +32,12 @@ const Routes = () => {
const canAccessAdmin = permissions?.canAccessAdmin;
const config = useConfig();
const {
admin: {
user: userSlug,
logoutRoute,
inactivityRoute: logoutInactivityRoute,
components: {
routes: customRoutes,
} = {},
@@ -42,7 +45,8 @@ const Routes = () => {
routes,
collections,
globals,
} = useConfig();
} = config;
const userCollection = collections.find(({ slug }) => slug === userSlug);
@@ -103,10 +107,10 @@ const Routes = () => {
<Route path={`${match.url}/login`}>
<Login />
</Route>
<Route path={`${match.url}/logout`}>
<Route path={`${match.url}${logoutRoute}`}>
<Logout />
</Route>
<Route path={`${match.url}/logout-inactivity`}>
<Route path={`${match.url}${logoutInactivityRoute}`}>
<Logout inactivity />
</Route>

View File

@@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom';
import { toast } from 'react-toastify';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useConfig } from '../../utilities/Config';
import { useWatchForm, useFormModified } from '../../forms/Form/context';
import { useFormModified, useAllFormFields } from '../../forms/Form/context';
import { useLocale } from '../../utilities/Locale';
import { Props } from './types';
import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues';
@@ -17,7 +17,7 @@ const baseClass = 'autosave';
const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdatedAt }) => {
const { serverURL, routes: { api, admin } } = useConfig();
const { versions, getVersions } = useDocumentInfo();
const { fields, dispatchFields } = useWatchForm();
const [fields] = useAllFormFields();
const modified = useFormModified();
const locale = useLocale();
const { replace } = useHistory();
@@ -39,6 +39,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
const createCollectionDoc = useCallback(async () => {
const res = await fetch(`${serverURL}${api}/${collection.slug}?locale=${locale}&fallback-locale=null&depth=0&draft=true`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
@@ -94,6 +95,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
setTimeout(async () => {
const res = await fetch(url, {
method,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
@@ -112,7 +114,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
};
autosave();
}, [debouncedFields, modified, serverURL, api, collection, global, id, dispatchFields, getVersions, locale]);
}, [debouncedFields, modified, serverURL, api, collection, global, id, getVersions, locale]);
useEffect(() => {
if (versions?.docs?.[0]) {

View File

@@ -37,7 +37,15 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
locale,
depth: 0,
});
const data = await response.json();
let data = await response.json();
if (typeof collection.admin.hooks?.beforeDuplicate === 'function') {
data = await collection.admin.hooks.beforeDuplicate({
data,
locale,
});
}
const result = await requests.post(`${serverURL}${api}/${slug}`, {
headers: {
'Content-Type': 'application/json',
@@ -65,7 +73,15 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
locale,
depth: 0,
});
const localizedDoc = await res.json();
let localizedDoc = await res.json();
if (typeof collection.admin.hooks?.beforeDuplicate === 'function') {
localizedDoc = await collection.admin.hooks.beforeDuplicate({
data: localizedDoc,
locale,
});
}
const patchResult = await requests.patch(`${serverURL}${api}/${slug}/${duplicateID}?locale=${locale}`, {
headers: {
'Content-Type': 'application/json',
@@ -97,7 +113,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
pathname: `${admin}/collections/${slug}/${duplicateID}`,
});
}, 10);
}, [modified, localization, collection.labels.singular, setModified, toggleModal, modalSlug, serverURL, api, slug, id, push, admin]);
}, [modified, localization, collection, setModified, toggleModal, modalSlug, serverURL, api, slug, id, push, admin]);
const confirm = useCallback(async () => {
setHasClicked(false);

View File

@@ -102,7 +102,7 @@ const FileDetails: React.FC<Props> = (props) => {
</div>
<Meta
{...val}
mimeType={mimeType}
mimeType={val.mimeType}
staticURL={staticURL}
/>
</li>

View File

@@ -0,0 +1,11 @@
import { Field, FieldAffectingData, fieldAffectsData } from '../../../../fields/config/types';
import flattenFields from '../../../../utilities/flattenTopLevelFields';
export const getTextFieldsToBeSearched = (listSearchableFields: string[], fields: Field[]) => (): FieldAffectingData[] => {
if (listSearchableFields) {
const flattenedFields = flattenFields(fields);
return flattenedFields.filter((field) => fieldAffectsData(field) && listSearchableFields.includes(field.name)) as FieldAffectingData[];
}
return null;
};

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import AnimateHeight from 'react-animate-height';
import { FieldAffectingData, fieldAffectsData } from '../../../../fields/config/types';
import { fieldAffectsData } from '../../../../fields/config/types';
import SearchFilter from '../SearchFilter';
import ColumnSelector from '../ColumnSelector';
import WhereBuilder from '../WhereBuilder';
@@ -8,8 +8,8 @@ import SortComplex from '../SortComplex';
import Button from '../Button';
import { Props } from './types';
import { useSearchParams } from '../../utilities/SearchParams';
import validateWhereQuery from '../WhereBuilder/validateWhereQuery';
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched';
import './index.scss';
@@ -38,7 +38,7 @@ const ListControls: React.FC<Props> = (props) => {
const shouldInitializeWhereOpened = validateWhereQuery(params?.where);
const [titleField] = useState(() => fields.find((field) => fieldAffectsData(field) && field.name === useAsTitle));
const [textFieldsToBeSearched] = useState(listSearchableFields ? () => fields.filter((field) => fieldAffectsData(field) && listSearchableFields.includes(field.name)) as FieldAffectingData[] : null);
const [textFieldsToBeSearched] = useState(getTextFieldsToBeSearched(listSearchableFields, fields));
const [visibleDrawer, setVisibleDrawer] = useState<'where' | 'sort' | 'columns'>(shouldInitializeWhereOpened ? 'where' : undefined);
return (

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useConfig } from '../../utilities/Config';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
import LogOut from '../../icons/LogOut';
const baseClass = 'nav';
const DefaultLogout = () => {
const config = useConfig();
const {
routes: { admin },
admin: {
logoutRoute,
components: { logout }
}
} = config;
return (
<Link to={`${admin}${logoutRoute}`} className={`${baseClass}__log-out`}>
<LogOut />
</Link>
);
};
const Logout: React.FC = () => {
const {
admin: {
components: {
logout: { Button: CustomLogout } = {
Button: undefined,
},
} = {},
} = {},
} = useConfig();
return (
<RenderCustomComponent
CustomComponent={CustomLogout}
DefaultComponent={DefaultLogout}
/>
);
};
export default Logout;

View File

@@ -6,7 +6,7 @@
top: 0;
left: 0;
height: 100vh;
width: base(9);
width: var(--nav-width);
overflow: hidden;
border-right: 1px solid var(--theme-elevation-100);
@@ -120,10 +120,6 @@
}
}
@include large-break {
width: base(8);
}
@include mid-break {
@include blur-bg;
position: fixed;

View File

@@ -4,7 +4,6 @@ import { useConfig } from '../../utilities/Config';
import { useAuth } from '../../utilities/Auth';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
import Chevron from '../../icons/Chevron';
import LogOut from '../../icons/LogOut';
import Menu from '../../icons/Menu';
import CloseMenu from '../../icons/CloseMenu';
import Icon from '../../graphics/Icon';
@@ -14,6 +13,7 @@ import NavGroup from '../NavGroup';
import { groupNavItems, Group, EntityToGroup, EntityType } from '../../../utilities/groupNavItems';
import './index.scss';
import Logout from '../Logout';
const baseClass = 'nav';
@@ -31,7 +31,7 @@ const DefaultNav = () => {
admin: {
components: {
beforeNavLinks,
afterNavLinks,
afterNavLinks
},
},
} = useConfig();
@@ -137,12 +137,7 @@ const DefaultNav = () => {
>
<Account />
</Link>
<Link
to={`${admin}/logout`}
className={`${baseClass}__log-out`}
>
<LogOut />
</Link>
<Logout/>
</div>
</nav>
</div>

View File

@@ -2,9 +2,10 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useWindowInfo } from '@faceless-ui/window-info';
import { Props } from './types';
import PopupButton from './PopupButton';
import useIntersect from '../../../hooks/useIntersect';
import './index.scss';
import useIntersect from '../../../hooks/useIntersect';
import useThrottledEffect from '../../../hooks/useThrottledEffect';
const baseClass = 'popup';
@@ -41,26 +42,20 @@ const Popup: React.FC<Props> = (props) => {
const [verticalAlign, setVerticalAlign] = useState(verticalAlignFromProps);
const [horizontalAlign, setHorizontalAlign] = useState(horizontalAlignFromProps);
const handleClickOutside = useCallback((e) => {
if (contentRef.current.contains(e.target)) {
return;
}
setActive(false);
}, [contentRef]);
useEffect(() => {
const setPosition = useCallback(({ horizontal = false, vertical = false }) => {
if (contentRef.current) {
const bounds = contentRef.current.getBoundingClientRect();
const {
left: contentLeftPos,
right: contentRightPos,
top: contentTopPos,
bottom: contentBottomPos,
} = contentRef.current.getBoundingClientRect();
} = bounds;
let boundingTopPos = 100;
let boundingRightPos = windowWidth;
let boundingBottomPos = windowHeight;
let boundingRightPos = window.innerWidth;
let boundingBottomPos = window.innerHeight;
let boundingLeftPos = 0;
if (boundingRef?.current) {
@@ -72,19 +67,39 @@ const Popup: React.FC<Props> = (props) => {
} = boundingRef.current.getBoundingClientRect());
}
if (contentRightPos > boundingRightPos && contentLeftPos > boundingLeftPos) {
setHorizontalAlign('right');
} else if (contentLeftPos < boundingLeftPos && contentRightPos < boundingRightPos) {
setHorizontalAlign('left');
if (horizontal) {
if (contentRightPos > boundingRightPos && contentLeftPos > boundingLeftPos) {
setHorizontalAlign('right');
} else if (contentLeftPos < boundingLeftPos && contentRightPos < boundingRightPos) {
setHorizontalAlign('left');
}
}
if (contentTopPos < boundingTopPos && contentBottomPos < boundingBottomPos) {
setVerticalAlign('bottom');
} else if (contentBottomPos > boundingBottomPos && contentTopPos > boundingTopPos) {
setVerticalAlign('top');
if (vertical) {
if (contentTopPos < boundingTopPos && contentBottomPos < boundingBottomPos) {
setVerticalAlign('bottom');
} else if (contentBottomPos > boundingBottomPos && contentTopPos > boundingTopPos) {
setVerticalAlign('top');
}
}
}
}, [boundingRef, intersectionEntry, windowHeight, windowWidth]);
}, [boundingRef]);
const handleClickOutside = useCallback((e) => {
if (contentRef.current.contains(e.target)) {
return;
}
setActive(false);
}, [contentRef]);
useEffect(() => {
setPosition({ horizontal: true });
}, [intersectionEntry, setPosition, windowWidth]);
useEffect(() => {
setPosition({ vertical: true });
}, [intersectionEntry, setPosition, windowHeight]);
useEffect(() => {
if (typeof onToggleOpen === 'function') onToggleOpen(active);

View File

@@ -116,4 +116,8 @@ div.react-select {
background-color: var(--theme-error-200);
}
}
}
&.rs--is-disabled .rs__control {
background: var(--theme-elevation-200);
}
}

View File

@@ -99,6 +99,7 @@ const ReactSelect: React.FC<Props> = (props) => {
disabled={disabled ? 'disabled' : undefined}
className={classes}
classNamePrefix="rs"
captureMenuScroll
options={options}
isSearchable={isSearchable}
isClearable={isClearable}
@@ -117,6 +118,7 @@ const ReactSelect: React.FC<Props> = (props) => {
return (
<Select
placeholder={placeholder}
captureMenuScroll
{...props}
value={value}
onChange={onChange}

View File

@@ -5,7 +5,7 @@ import IDLabel from '../IDLabel';
const baseClass = 'render-title';
const RenderTitle : React.FC<Props> = (props) => {
const RenderTitle: React.FC<Props> = (props) => {
const {
useAsTitle,
title: titleFromProps,
@@ -14,10 +14,8 @@ const RenderTitle : React.FC<Props> = (props) => {
} = props;
const titleFromForm = useTitle(useAsTitle);
const titleFromData = data && data[useAsTitle];
let title = titleFromData;
if (!title) title = titleFromForm;
let title = titleFromForm;
if (!title) title = data?.id;
if (!title) title = fallback;
title = titleFromProps || title;

View File

@@ -68,8 +68,9 @@ const SearchFilter: React.FC<Props> = (props) => {
where: newWhere,
}),
});
setPreviousSearch(debouncedSearch);
}
setPreviousSearch(debouncedSearch);
}
}, [debouncedSearch, previousSearch, history, fieldName, params, handleChange, modifySearchQuery, listSearchableFields]);

View File

@@ -12,8 +12,8 @@ const UploadGallery: React.FC<Props> = (props) => {
if (docs && docs.length > 0) {
return (
<ul className={baseClass}>
{docs.map((doc, i) => (
<li key={i}>
{docs.map((doc) => (
<li key={String(doc.id)}>
<UploadCard
doc={doc}
{...{ collection }}

View File

@@ -60,7 +60,7 @@ const RelationshipField: React.FC<Props> = (props) => {
const fieldToSearch = collection?.admin?.useAsTitle || 'id';
const searchParam = searchArg ? `&where[${fieldToSearch}][like]=${searchArg}` : '';
const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`);
const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`, { credentials: 'include' });
if (response.ok) {
const data: PaginatedDocs<any> = await response.json();
@@ -152,7 +152,7 @@ const RelationshipField: React.FC<Props> = (props) => {
const addOptionByID = useCallback(async (id, relation) => {
if (!errorLoading && id !== 'null') {
const response = await fetch(`${serverURL}${api}/${relation}/${id}?depth=0`);
const response = await fetch(`${serverURL}${api}/${relation}/${id}?depth=0`, { credentials: 'include' });
if (response.ok) {
const data = await response.json();

View File

@@ -1,11 +1,13 @@
import { createContext, useContext } from 'react';
import { Context } from './types';
import { useContextSelector, createContext as createSelectorContext, useContext as useFullContext } from 'use-context-selector';
import { Context, FormFieldsContext as FormFieldsContextType } from './types';
const FormContext = createContext({} as Context);
const FormWatchContext = createContext({} as Context);
const SubmittedContext = createContext(false);
const ProcessingContext = createContext(false);
const ModifiedContext = createContext(false);
const FormFieldsContext = createSelectorContext<FormFieldsContextType>([{}, () => null]);
const useForm = (): Context => useContext(FormContext);
const useWatchForm = (): Context => useContext(FormWatchContext);
@@ -13,15 +15,21 @@ const useFormSubmitted = (): boolean => useContext(SubmittedContext);
const useFormProcessing = (): boolean => useContext(ProcessingContext);
const useFormModified = (): boolean => useContext(ModifiedContext);
const useFormFields = <Value = unknown>(selector: (context: FormFieldsContextType) => Value): Value => useContextSelector(FormFieldsContext, selector);
const useAllFormFields = (): FormFieldsContextType => useFullContext(FormFieldsContext);
export {
FormContext,
FormWatchContext,
SubmittedContext,
ProcessingContext,
ModifiedContext,
useForm,
useWatchForm,
useFormSubmitted,
useFormProcessing,
useFormModified,
useForm,
FormContext,
FormFieldsContext,
useFormFields,
useAllFormFields,
FormWatchContext,
useWatchForm,
};

View File

@@ -1,43 +1,11 @@
import equal from 'deep-equal';
import { unflatten, flatten } from 'flatley';
import flattenFilters from './flattenFilters';
import getSiblingData from './getSiblingData';
import reduceFieldsToValues from './reduceFieldsToValues';
import { Fields } from './types';
import { Field, FieldAction, Fields } from './types';
import deepCopyObject from '../../../../utilities/deepCopyObject';
import { flattenRows, separateRows } from './rows';
const unflattenRowsFromState = (state: Fields, path) => {
// Take a copy of state
const remainingFlattenedState = { ...state };
const rowsFromStateObject = {};
const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1);
// Loop over all keys from state
// If the key begins with the name of the parent field,
// Add value to rowsFromStateObject and delete it from remaining state
Object.keys(state).forEach((key) => {
if (key.indexOf(`${path}.`) === 0) {
if (!state[key].disableFormData) {
const name = key.replace(pathPrefixToRemove, '');
rowsFromStateObject[name] = state[key];
rowsFromStateObject[name].initialValue = rowsFromStateObject[name].value;
}
delete remainingFlattenedState[key];
}
});
const unflattenedRows = unflatten(rowsFromStateObject);
return {
unflattenedRows: unflattenedRows[path.replace(pathPrefixToRemove, '')] || [],
remainingFlattenedState,
};
};
function fieldReducer(state: Fields, action): Fields {
function fieldReducer(state: Fields, action: FieldAction): Fields {
switch (action.type) {
case 'REPLACE_STATE': {
const newState = {};
@@ -70,23 +38,27 @@ function fieldReducer(state: Fields, action): Fields {
case 'REMOVE_ROW': {
const { rowIndex, path } = action;
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
const { remainingFields, rows } = separateRows(path, state);
unflattenedRows.splice(rowIndex, 1);
rows.splice(rowIndex, 1);
const flattenedRowState = unflattenedRows.length > 0 ? flatten({ [path]: unflattenedRows }, { filters: flattenFilters }) : {};
return {
...remainingFlattenedState,
...flattenedRowState,
const newState: Fields = {
...remainingFields,
[path]: {
...state[path],
value: rows.length,
disableFormData: rows.length > 0,
},
...flattenRows(path, rows),
};
return newState;
}
case 'ADD_ROW': {
const {
rowIndex, path, subFieldState, blockType,
} = action;
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
if (blockType) {
subFieldState.blockType = {
@@ -96,15 +68,18 @@ function fieldReducer(state: Fields, action): Fields {
};
}
// If there are subfields
if (Object.keys(subFieldState).length > 0) {
// Add new object containing subfield names to unflattenedRows array
unflattenedRows.splice(rowIndex + 1, 0, subFieldState);
}
const { remainingFields, rows } = separateRows(path, state);
rows.splice(rowIndex + 1, 0, subFieldState);
const newState = {
...remainingFlattenedState,
...(flatten({ [path]: unflattenedRows }, { filters: flattenFilters })),
...remainingFields,
[path]: {
...state[path],
value: rows.length,
disableFormData: true,
},
...flattenRows(path, rows),
};
return newState;
@@ -115,20 +90,25 @@ function fieldReducer(state: Fields, action): Fields {
rowIndex, path,
} = action;
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
const { remainingFields, rows } = separateRows(path, state);
const duplicate = deepCopyObject(unflattenedRows[rowIndex]);
const duplicate = deepCopyObject(rows[rowIndex]);
if (duplicate.id) delete duplicate.id;
// If there are subfields
if (Object.keys(duplicate).length > 0) {
// Add new object containing subfield names to unflattenedRows array
unflattenedRows.splice(rowIndex + 1, 0, duplicate);
rows.splice(rowIndex + 1, 0, duplicate);
}
const newState = {
...remainingFlattenedState,
...(flatten({ [path]: unflattenedRows }, { filters: flattenFilters })),
...remainingFields,
[path]: {
...state[path],
value: rows.length,
disableFormData: true,
},
...flattenRows(path, rows),
};
return newState;
@@ -136,18 +116,18 @@ function fieldReducer(state: Fields, action): Fields {
case 'MOVE_ROW': {
const { moveFromIndex, moveToIndex, path } = action;
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
const { remainingFields, rows } = separateRows(path, state);
// copy the row to move
const copyOfMovingRow = unflattenedRows[moveFromIndex];
const copyOfMovingRow = rows[moveFromIndex];
// delete the row by index
unflattenedRows.splice(moveFromIndex, 1);
rows.splice(moveFromIndex, 1);
// insert row copyOfMovingRow back in
unflattenedRows.splice(moveToIndex, 0, copyOfMovingRow);
rows.splice(moveToIndex, 0, copyOfMovingRow);
const newState = {
...remainingFlattenedState,
...(flatten({ [path]: unflattenedRows }, { filters: flattenFilters })),
...remainingFields,
...flattenRows(path, rows),
};
return newState;
@@ -186,23 +166,27 @@ function fieldReducer(state: Fields, action): Fields {
}, {});
}
default: {
const newField = {
value: action.value,
valid: action.valid,
errorMessage: action.errorMessage,
disableFormData: action.disableFormData,
initialValue: action.initialValue,
validate: action.validate,
condition: action.condition,
passesCondition: action.passesCondition,
};
case 'UPDATE': {
const newField = Object.entries(action).reduce((field, [key, value]) => {
if (['value', 'valid', 'errorMessage', 'disableFormData', 'initialValue', 'validate', 'condition', 'passesCondition'].includes(key)) {
return {
...field,
[key]: value,
};
}
return field;
}, state[action.path] || {} as Field);
return {
...state,
[action.path]: newField,
};
}
default: {
return state;
}
}
}

View File

@@ -1,10 +0,0 @@
const flattenFilters = [{
test: (_: string, value: unknown): boolean => {
const hasValidProperty = Object.prototype.hasOwnProperty.call(value, 'valid');
const hasValueProperty = Object.prototype.hasOwnProperty.call(value, 'value');
return (hasValidProperty && hasValueProperty);
},
}];
export default flattenFilters;

View File

@@ -21,7 +21,7 @@ import { Field } from '../../../../fields/config/types';
import buildInitialState from './buildInitialState';
import errorMessages from './errorMessages';
import { Context as FormContextType, GetDataByPath, Props, SubmitOptions } from './types';
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FormWatchContext } from './context';
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FormFieldsContext, FormWatchContext } from './context';
import buildStateFromSchema from './buildStateFromSchema';
import { useOperation } from '../../utilities/OperationProvider';
@@ -63,12 +63,11 @@ const Form: React.FC<Props> = (props) => {
if (formattedInitialData) initialFieldState = formattedInitialData;
if (initialState) initialFieldState = initialState;
// Allow access to initialState for field types such as Blocks and Array
contextRef.current.initialState = initialState;
const [fields, dispatchFields] = useReducer(fieldReducer, {}, () => initialFieldState);
const fieldsReducer = useReducer(fieldReducer, {}, () => initialFieldState);
const [fields, dispatchFields] = fieldsReducer;
contextRef.current.fields = fields;
contextRef.current.dispatchFields = dispatchFields;
const validateForm = useCallback(async () => {
const validatedFieldState = {};
@@ -111,7 +110,7 @@ const Form: React.FC<Props> = (props) => {
}
return isValid;
}, [contextRef, id, user, operation]);
}, [contextRef, id, user, operation, dispatchFields]);
const submit = useCallback(async (options: SubmitOptions = {}, e): Promise<void> => {
const {
@@ -254,6 +253,7 @@ const Form: React.FC<Props> = (props) => {
fieldErrors.forEach((err) => {
dispatchFields({
type: 'UPDATE',
...(contextRef.current?.fields?.[err.field] || {}),
valid: false,
errorMessage: err.message,
@@ -268,7 +268,7 @@ const Form: React.FC<Props> = (props) => {
return;
}
const message = errorMessages[res.status] || 'An unknown error occurrred.';
const message = errorMessages[res.status] || 'An unknown error occurred.';
toast.error(message);
}
@@ -283,6 +283,7 @@ const Form: React.FC<Props> = (props) => {
action,
disableSuccessStatus,
disabled,
dispatchFields,
fields,
handleResponse,
history,
@@ -298,7 +299,6 @@ const Form: React.FC<Props> = (props) => {
const getData = useCallback(() => reduceFieldsToValues(contextRef.current.fields, true), [contextRef]);
const getSiblingData = useCallback((path: string) => getSiblingDataFunc(contextRef.current.fields, path), [contextRef]);
const getDataByPath = useCallback<GetDataByPath>((path: string) => getDataByPathFunc(contextRef.current.fields, path), [contextRef]);
const getUnflattenedValues = useCallback(() => reduceFieldsToValues(contextRef.current.fields), [contextRef]);
const createFormData = useCallback((overrides: any = {}) => {
const data = reduceFieldsToValues(contextRef.current.fields, true);
@@ -328,16 +328,14 @@ const Form: React.FC<Props> = (props) => {
const state = await buildStateFromSchema({ fieldSchema, data, user, id, operation, locale });
contextRef.current = { ...initContextState } as FormContextType;
dispatchFields({ type: 'REPLACE_STATE', state });
}, [id, user, operation, locale]);
}, [id, user, operation, locale, dispatchFields]);
contextRef.current.dispatchFields = dispatchFields;
contextRef.current.submit = submit;
contextRef.current.getFields = getFields;
contextRef.current.getField = getField;
contextRef.current.getData = getData;
contextRef.current.getSiblingData = getSiblingData;
contextRef.current.getDataByPath = getDataByPath;
contextRef.current.getUnflattenedValues = getUnflattenedValues;
contextRef.current.validateForm = validateForm;
contextRef.current.createFormData = createFormData;
contextRef.current.setModified = setModified;
@@ -352,7 +350,7 @@ const Form: React.FC<Props> = (props) => {
contextRef.current = { ...initContextState } as FormContextType;
dispatchFields({ type: 'REPLACE_STATE', state: initialState });
}
}, [initialState]);
}, [initialState, dispatchFields]);
useEffect(() => {
if (initialData) {
@@ -361,20 +359,12 @@ const Form: React.FC<Props> = (props) => {
setFormattedInitialData(builtState);
dispatchFields({ type: 'REPLACE_STATE', state: builtState });
}
}, [initialData]);
}, [initialData, dispatchFields]);
useThrottledEffect(() => {
refreshCookie();
}, 15000, [fields]);
// Re-run form validation every second
// as fields change, because field validations can
// potentially rely on OTHER field values to determine
// if they are valid or not (siblingData, data)
useThrottledEffect(() => {
validateForm();
}, 1000, [validateForm, fields]);
useEffect(() => {
contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form
setModified(false);
@@ -403,7 +393,9 @@ const Form: React.FC<Props> = (props) => {
<SubmittedContext.Provider value={submitted}>
<ProcessingContext.Provider value={processing}>
<ModifiedContext.Provider value={modified}>
{children}
<FormFieldsContext.Provider value={fieldsReducer}>
{children}
</FormFieldsContext.Provider>
</ModifiedContext.Provider>
</ProcessingContext.Provider>
</SubmittedContext.Provider>

View File

@@ -6,7 +6,6 @@ import {
Submit,
Context,
GetSiblingData,
GetUnflattenedValues,
ValidateForm,
CreateFormData,
SetModified,
@@ -17,7 +16,6 @@ import {
const submit: Submit = () => undefined;
const getSiblingData: GetSiblingData = () => undefined;
const getUnflattenedValues: GetUnflattenedValues = () => ({});
const dispatchFields: DispatchFields = () => undefined;
const validateForm: ValidateForm = () => undefined;
const createFormData: CreateFormData = () => undefined;
@@ -28,12 +26,11 @@ const setSubmitted: SetSubmitted = () => undefined;
const reset: Reset = () => undefined;
const initialContextState: Context = {
getFields: (): Fields => ({ }),
getFields: (): Fields => ({}),
getField: (): Field => undefined,
getData: (): Data => undefined,
getSiblingData,
getDataByPath: () => undefined,
getUnflattenedValues,
validateForm,
createFormData,
submit,
@@ -41,7 +38,6 @@ const initialContextState: Context = {
setModified,
setProcessing,
setSubmitted,
initialState: {},
fields: {},
disabled: false,
formRef: null,

View File

@@ -0,0 +1,41 @@
import { Fields } from './types';
type Result = {
remainingFields: Fields
rows: Fields[]
}
export const separateRows = (path: string, fields: Fields): Result => {
const remainingFields: Fields = {};
const rows = Object.entries(fields).reduce((incomingRows, [fieldPath, field]) => {
const newRows = incomingRows;
if (fieldPath.indexOf(`${path}.`) === 0) {
const index = Number(fieldPath.replace(`${path}.`, '').split('.')[0]);
if (!newRows[index]) newRows[index] = {};
newRows[index][fieldPath.replace(`${path}.${String(index)}.`, '')] = field;
} else {
remainingFields[fieldPath] = field;
}
return newRows;
}, []);
return {
remainingFields,
rows,
};
};
export const flattenRows = (path: string, rows: Fields[]): Fields => {
return rows.reduce((fields, row, i) => ({
...fields,
...Object.entries(row).reduce((subFields, [subPath, subField]) => {
return {
...subFields,
[`${path}.${i}.${subPath}`]: subField,
};
}, {}),
}), {});
};

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { Dispatch } from 'react';
import { Field as FieldConfig, Condition, Validate } from '../../../../fields/config/types';
export type Field = {
@@ -50,14 +50,13 @@ export type SubmitOptions = {
}
export type DispatchFields = React.Dispatch<any>
export type Submit = (options?: SubmitOptions, e?: React.FormEvent<HTMLFormElement>) => void;
export type Submit = (options?: SubmitOptions, e?: React.FormEvent<HTMLFormElement>) => Promise<void>;
export type ValidateForm = () => Promise<boolean>;
export type CreateFormData = (overrides?: any) => FormData;
export type GetFields = () => Fields;
export type GetField = (path: string) => Field;
export type GetData = () => Data;
export type GetSiblingData = (path: string) => Data;
export type GetUnflattenedValues = () => Data;
export type GetDataByPath = <T = unknown>(path: string) => T;
export type SetModified = (modified: boolean) => void;
export type SetSubmitted = (submitted: boolean) => void;
@@ -65,11 +64,73 @@ export type SetProcessing = (processing: boolean) => void;
export type Reset = (fieldSchema: FieldConfig[], data: unknown) => Promise<void>
export type REPLACE_STATE = {
type: 'REPLACE_STATE'
state: Fields
}
export type REMOVE = {
type: 'REMOVE'
path: string
}
export type REMOVE_ROW = {
type: 'REMOVE_ROW'
rowIndex: number
path: string
}
export type ADD_ROW = {
type: 'ADD_ROW'
rowIndex: number
path: string
subFieldState?: Fields
blockType?: string
}
export type DUPLICATE_ROW = {
type: 'DUPLICATE_ROW'
rowIndex: number
path: string
}
export type MOVE_ROW = {
type: 'MOVE_ROW'
moveFromIndex: number
moveToIndex: number
path: string
}
export type MODIFY_CONDITION = {
type: 'MODIFY_CONDITION'
path: string
result: boolean
}
export type UPDATE = {
type: 'UPDATE'
path: string
} & Partial<Field>
export type FieldAction =
| REPLACE_STATE
| REMOVE
| REMOVE_ROW
| ADD_ROW
| DUPLICATE_ROW
| MOVE_ROW
| MODIFY_CONDITION
| UPDATE
export type FormFieldsContext = [Fields, Dispatch<FieldAction>]
export type Context = {
dispatchFields: DispatchFields
submit: Submit
/**
* @deprecated Form context fields may be outdated and should not be relied on. Instead, prefer `useFormFields`.
*/
fields: Fields
initialState: Fields
submit: Submit
dispatchFields: Dispatch<FieldAction>
validateForm: ValidateForm
createFormData: CreateFormData
disabled: boolean
@@ -77,7 +138,6 @@ export type Context = {
getField: GetField
getData: GetData
getSiblingData: GetSiblingData
getUnflattenedValues: GetUnflattenedValues
getDataByPath: GetDataByPath
setModified: SetModified
setProcessing: SetProcessing

View File

@@ -20,6 +20,7 @@ const RenderFields: React.FC<Props> = (props) => {
readOnly: readOnlyOverride,
className,
forceRender,
indexPath: incomingIndexPath
} = props;
const [hasRendered, setHasRendered] = useState(Boolean(forceRender));
@@ -48,7 +49,7 @@ const RenderFields: React.FC<Props> = (props) => {
className={classes}
>
{hasRendered && (
fieldSchema.map((field, i) => {
fieldSchema.map((field, fieldIndex) => {
const fieldIsPresentational = fieldIsPresentationalOnly(field);
let FieldComponent = fieldTypes[field.type];
@@ -58,7 +59,7 @@ const RenderFields: React.FC<Props> = (props) => {
return (
<FieldComponent
{...field}
key={i}
key={fieldIndex}
/>
);
}
@@ -83,13 +84,14 @@ const RenderFields: React.FC<Props> = (props) => {
if (FieldComponent) {
return (
<RenderCustomComponent
key={i}
key={fieldIndex}
CustomComponent={field?.admin?.components?.Field}
DefaultComponent={FieldComponent}
componentProps={{
...field,
path: field.path || (isFieldAffectingData ? field.name : ''),
fieldTypes,
indexPath: incomingIndexPath ? `${incomingIndexPath}.${fieldIndex}` : `${fieldIndex}`,
admin: {
...(field.admin || {}),
readOnly,
@@ -103,7 +105,7 @@ const RenderFields: React.FC<Props> = (props) => {
return (
<div
className="missing-field"
key={i}
key={fieldIndex}
>
No matched field found for
{' '}

View File

@@ -6,10 +6,11 @@ export type Props = {
className?: string
readOnly?: boolean
forceRender?: boolean
permissions?: {
permissions?: FieldPermissions | {
[field: string]: FieldPermissions
}
filter?: (field: Field) => boolean
fieldSchema: FieldWithPath[]
fieldTypes: FieldTypes
indexPath?: string
}

View File

@@ -38,6 +38,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
maxRows,
minRows,
permissions,
indexPath,
admin: {
readOnly,
description,
@@ -70,57 +71,55 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const locale = useLocale();
const operation = useOperation();
const { dispatchFields } = formContext;
const { dispatchFields, setModified } = formContext;
const memoizedValidate = useCallback((value, options) => {
return validate(value, { ...options, minRows, maxRows, required });
}, [maxRows, minRows, required, validate]);
const [disableFormData, setDisableFormData] = useState(false);
const {
showError,
errorMessage,
value,
setValue,
} = useField({
path,
validate: memoizedValidate,
disableFormData,
condition,
disableFormData: rows?.length > 0,
});
const addRow = useCallback(async (rowIndex: number) => {
const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user, locale });
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path });
dispatchRows({ type: 'ADD', rowIndex });
setValue(value as number + 1);
setModified(true);
setTimeout(() => {
scrollToID(`${path}-row-${rowIndex + 1}`);
}, 0);
}, [dispatchRows, dispatchFields, fields, path, setValue, value, operation, id, user, locale]);
}, [dispatchRows, dispatchFields, fields, path, operation, id, user, locale, setModified]);
const duplicateRow = useCallback(async (rowIndex: number) => {
dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path });
dispatchRows({ type: 'ADD', rowIndex });
setValue(value as number + 1);
setModified(true);
setTimeout(() => {
scrollToID(`${path}-row-${rowIndex + 1}`);
}, 0);
}, [dispatchRows, dispatchFields, path, setValue, value]);
}, [dispatchRows, dispatchFields, path, setModified]);
const removeRow = useCallback((rowIndex: number) => {
dispatchRows({ type: 'REMOVE', rowIndex });
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
setValue(value as number - 1);
}, [dispatchRows, dispatchFields, path, value, setValue]);
setModified(true);
}, [dispatchRows, dispatchFields, path, setModified]);
const moveRow = useCallback((moveFromIndex: number, moveToIndex: number) => {
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
}, [dispatchRows, dispatchFields, path]);
setModified(true);
}, [dispatchRows, dispatchFields, path, setModified]);
const onDragEnd = useCallback((result) => {
if (!result.destination) return;
@@ -193,16 +192,6 @@ const ArrayFieldType: React.FC<Props> = (props) => {
initializeRowState();
}, [formContext, path, getPreference, preferencesKey, initCollapsed]);
useEffect(() => {
setValue(rows?.length || 0, true);
if (rows?.length === 0) {
setDisableFormData(false);
} else {
setDisableFormData(true);
}
}, [rows, setValue]);
const hasMaxRows = maxRows && rows?.length >= maxRows;
const classes = [
@@ -304,6 +293,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
readOnly={readOnly}
fieldTypes={fieldTypes}
permissions={permissions?.fields}
indexPath={indexPath}
fieldSchema={fields.map((field) => ({
...field,
path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`,

View File

@@ -7,4 +7,5 @@ export type Props = Omit<ArrayField, 'type'> & {
fieldTypes: FieldTypes
permissions: FieldPermissions
label: string | false
indexPath: string
}

View File

@@ -37,7 +37,7 @@ const labelDefaults = {
plural: 'Blocks',
};
const Index: React.FC<Props> = (props) => {
const BlocksField: React.FC<Props> = (props) => {
const {
label,
name,
@@ -50,6 +50,7 @@ const Index: React.FC<Props> = (props) => {
required,
validate = blocksValidator,
permissions,
indexPath,
admin: {
readOnly,
description,
@@ -70,25 +71,23 @@ const Index: React.FC<Props> = (props) => {
const { id } = useDocumentInfo();
const locale = useLocale();
const operation = useOperation();
const { dispatchFields } = formContext;
const { dispatchFields, setModified } = formContext;
const [selectorIndexOpen, setSelectorIndexOpen] = useState<number>();
const memoizedValidate = useCallback((value, options) => {
return validate(value, { ...options, minRows, maxRows, required });
}, [maxRows, minRows, required, validate]);
const [disableFormData, setDisableFormData] = useState(false);
const [selectorIndexOpen, setSelectorIndexOpen] = useState<number>();
const {
showError,
errorMessage,
value,
setValue,
} = useField<number>({
path,
validate: memoizedValidate,
disableFormData,
condition,
disableFormData: rows?.length > 0,
});
const onAddPopupToggle = useCallback((open) => {
@@ -102,33 +101,34 @@ const Index: React.FC<Props> = (props) => {
const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user, locale });
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path, blockType });
dispatchRows({ type: 'ADD', rowIndex, blockType });
setValue(value as number + 1);
setModified(true);
setTimeout(() => {
scrollToID(`${path}-row-${rowIndex + 1}`);
}, 0);
}, [path, setValue, value, blocks, dispatchFields, operation, id, user, locale]);
}, [path, blocks, dispatchFields, operation, id, user, locale, setModified]);
const duplicateRow = useCallback(async (rowIndex: number, blockType: string) => {
dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path });
dispatchRows({ type: 'ADD', rowIndex, blockType });
setValue(value as number + 1);
setModified(true);
setTimeout(() => {
scrollToID(`${path}-row-${rowIndex + 1}`);
}, 0);
}, [dispatchRows, dispatchFields, path, setValue, value]);
}, [dispatchRows, dispatchFields, path, setModified]);
const removeRow = useCallback((rowIndex: number) => {
dispatchRows({ type: 'REMOVE', rowIndex });
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
setValue(value as number - 1);
}, [path, setValue, value, dispatchFields]);
setModified(true);
}, [path, dispatchFields, setModified]);
const moveRow = useCallback((moveFromIndex: number, moveToIndex: number) => {
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
}, [dispatchRows, dispatchFields, path]);
setModified(true);
}, [dispatchRows, dispatchFields, path, setModified]);
const onDragEnd = useCallback((result) => {
if (!result.destination) return;
@@ -199,16 +199,6 @@ const Index: React.FC<Props> = (props) => {
initializeRowState();
}, [formContext, path, getPreference, preferencesKey, initCollapsed]);
useEffect(() => {
setValue(rows?.length || 0, true);
if (rows?.length === 0) {
setDisableFormData(false);
} else {
setDisableFormData(true);
}
}, [rows, setValue]);
const hasMaxRows = maxRows && rows?.length >= maxRows;
const classes = [
@@ -356,6 +346,7 @@ const Index: React.FC<Props> = (props) => {
...field,
path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`,
}))}
indexPath={indexPath}
/>
</Collapsible>
@@ -419,4 +410,4 @@ const Index: React.FC<Props> = (props) => {
);
};
export default withCondition(Index);
export default withCondition(BlocksField);

View File

@@ -6,4 +6,5 @@ export type Props = Omit<BlockField, 'type'> & {
path?: string
fieldTypes: FieldTypes
permissions: FieldPermissions
indexPath: string
}

View File

@@ -60,7 +60,6 @@ const Code: React.FC<Props> = (props) => {
} = useField({
path,
validate: memoizedValidate,
enableDebouncedValue: true,
condition,
});

View File

@@ -21,6 +21,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
fieldTypes,
path,
permissions,
indexPath,
admin: {
readOnly,
className,
@@ -75,8 +76,9 @@ const CollapsibleField: React.FC<Props> = (props) => {
<RenderFields
forceRender
readOnly={readOnly}
permissions={permissions?.fields}
permissions={permissions}
fieldTypes={fieldTypes}
indexPath={indexPath}
fieldSchema={fields.map((field) => ({
...field,
path: getFieldPath(path, field),

View File

@@ -6,4 +6,5 @@ export type Props = Omit<CollapsibleField, 'type'> & {
path?: string
fieldTypes: FieldTypes
permissions: FieldPermissions
indexPath: string
}

View File

@@ -2,15 +2,15 @@ import React, { useCallback } from 'react';
import useField from '../../useField';
import Label from '../../Label';
import Error from '../../Error';
import { useWatchForm } from '../../Form/context';
import { useFormFields } from '../../Form/context';
import './index.scss';
import { Field } from '../../Form/types';
const ConfirmPassword: React.FC = () => {
const { getField } = useWatchForm();
const password = getField('password');
const password = useFormFields<Field>(([fields]) => fields.password);
const validate = useCallback((value) => {
const validate = useCallback((value: string) => {
if (!value) {
return 'This field is required';
}
@@ -31,7 +31,6 @@ const ConfirmPassword: React.FC = () => {
path: 'confirm-password',
disableFormData: true,
validate,
enableDebouncedValue: true,
});
const classes = [

View File

@@ -37,7 +37,6 @@ const Email: React.FC<Props> = (props) => {
const fieldType = useField({
path,
validate: memoizedValidate,
enableDebouncedValue: true,
condition,
});

View File

@@ -73,7 +73,11 @@
}
}
.group-field + .group-field {
.group-field+.group-field {
margin-top: base(-2);
border-top: 0;
}
.group-field--within-collapsible+.group-field--within-collapsible {
margin-top: base(-1);
}

View File

@@ -19,6 +19,7 @@ const Group: React.FC<Props> = (props) => {
name,
path: pathFromProps,
fieldTypes,
indexPath,
admin: {
readOnly,
style,
@@ -70,6 +71,7 @@ const Group: React.FC<Props> = (props) => {
permissions={permissions?.fields}
readOnly={readOnly}
fieldTypes={fieldTypes}
indexPath={indexPath}
fieldSchema={fields.map((subField) => ({
...subField,
path: `${path}${fieldAffectsData(subField) ? `.${subField.name}` : ''}`,

View File

@@ -6,4 +6,5 @@ export type Props = Omit<GroupField, 'type'> & {
path?: string
fieldTypes: FieldTypes
permissions: FieldPermissions
indexPath: string
}

View File

@@ -44,7 +44,6 @@ const NumberField: React.FC<Props> = (props) => {
} = useField({
path,
validate: memoizedValidate,
enableDebouncedValue: true,
condition,
});

View File

@@ -37,7 +37,6 @@ const Password: React.FC<Props> = (props) => {
} = useField({
path,
validate: memoizedValidate,
enableDebouncedValue: true,
});
const classes = [

View File

@@ -44,7 +44,6 @@ const PointField: React.FC<Props> = (props) => {
} = useField<[number, number]>({
path,
validate: memoizedValidate,
enableDebouncedValue: true,
condition,
});

View File

@@ -0,0 +1,117 @@
@import '../../../../../../scss/styles.scss';
.relationship-add-new-modal {
display: flex;
overflow: hidden;
position: fixed;
height: 100vh;
&__blur-bg {
@include blur-bg();
position: absolute;
z-index: 1;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
transition: all 300ms ease-out;
}
.collection-edit {
@include blur-bg();
transform: translateX(#{base(4)});
opacity: 0;
transition: all 300ms ease-out;
position: relative;
z-index: 2;
}
.collection-edit__form {
overflow: auto;
position: relative;
z-index: 1;
}
.collection-edit__document-actions {
&:before,
&:after {
content: none;
}
}
&--animated {
.collection-edit,
.relationship-add-new-modal__blur-bg,
.relationship-add-new-modal__close {
opacity: 1;
}
.collection-edit {
transform: translateX(0);
}
}
.collection-edit__document-actions {
margin-top: base(2.75);
}
&__close {
@extend %btn-reset;
position: relative;
z-index: 2;
flex-shrink: 0;
text-indent: -9999px;
background: rgba(0, 0, 0, 0.08);
cursor: pointer;
opacity: 0;
transition: all 300ms ease-in-out;
transition-delay: 100ms;
&:active,
&:focus {
outline: 0;
}
}
&__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-top: base(2.5);
}
&__header-close {
svg {
width: base(2.5);
height: base(2.5);
position: relative;
top: base(-.5);
right: base(-.75);
.stroke {
stroke-width: .5px;
}
}
}
@include mid-break {
&__header-close {
svg {
top: base(-.75);
}
}
&__close {
width: base(1);
}
}
}
html[data-theme=dark] {
.relationship-add-new-modal__close {
background: rgba(0, 0, 0, 0.2);
}
}

View File

@@ -0,0 +1,109 @@
import React, { useEffect, useState } from 'react';
import { Modal, useModal } from '@faceless-ui/modal';
import { useWindowInfo } from '@faceless-ui/window-info';
import Button from '../../../../../elements/Button';
import { Props } from './types';
import { useAuth } from '../../../../../utilities/Auth';
import RenderCustomComponent from '../../../../../utilities/RenderCustomComponent';
import { useLocale } from '../../../../../utilities/Locale';
import { useConfig } from '../../../../../utilities/Config';
import DefaultEdit from '../../../../../views/collections/Edit/Default';
import X from '../../../../../icons/X';
import { Fields } from '../../../../Form/types';
import buildStateFromSchema from '../../../../Form/buildStateFromSchema';
import { EditDepthContext, useEditDepth } from '../../../../../utilities/EditDepth';
import { DocumentInfoProvider } from '../../../../../utilities/DocumentInfo';
import './index.scss';
const baseClass = 'relationship-add-new-modal';
export const AddNewRelationModal: React.FC<Props> = ({ modalCollection, onSave, modalSlug }) => {
const { serverURL, routes: { api } } = useConfig();
const { toggleModal } = useModal();
const { breakpoints: { m: midBreak } } = useWindowInfo();
const locale = useLocale();
const { permissions, user } = useAuth();
const [initialState, setInitialState] = useState<Fields>();
const [isAnimated, setIsAnimated] = useState(false);
const editDepth = useEditDepth();
const modalAction = `${serverURL}${api}/${modalCollection.slug}?locale=${locale}&depth=0&fallback-locale=null`;
useEffect(() => {
const buildState = async () => {
const state = await buildStateFromSchema({ fieldSchema: modalCollection.fields, data: {}, user, operation: 'create', locale });
setInitialState(state);
};
buildState();
}, [modalCollection, locale, user]);
useEffect(() => {
setIsAnimated(true);
}, []);
return (
<Modal
slug={modalSlug}
className={[
baseClass,
isAnimated && `${baseClass}--animated`,
].filter(Boolean).join(' ')}
>
{editDepth === 1 && (
<div className={`${baseClass}__blur-bg`} />
)}
<DocumentInfoProvider collection={modalCollection}>
<EditDepthContext.Provider value={editDepth + 1}>
<button
className={`${baseClass}__close`}
type="button"
onClick={() => toggleModal(modalSlug)}
style={{
width: `calc(${midBreak ? 'var(--gutter-h)' : 'var(--nav-width)'} + ${editDepth - 1} * 25px)`,
}}
>
<span>
Close
</span>
</button>
<RenderCustomComponent
DefaultComponent={DefaultEdit}
CustomComponent={modalCollection.admin?.components?.views?.Edit}
componentProps={{
isLoading: !initialState,
data: {},
collection: modalCollection,
permissions: permissions.collections[modalCollection.slug],
isEditing: false,
onSave,
initialState,
hasSavePermission: true,
action: modalAction,
disableEyebrow: true,
disableActions: true,
disableLeaveWithoutSaving: true,
customHeader: (
<div className={`${baseClass}__header`}>
<h2>
Add new
{' '}
{modalCollection.labels.singular}
</h2>
<Button
buttonStyle="none"
className={`${baseClass}__header-close`}
onClick={() => toggleModal(modalSlug)}
>
<X />
</Button>
</div>
),
}}
/>
</EditDepthContext.Provider>
</DocumentInfoProvider>
</Modal>
);
};

View File

@@ -0,0 +1,7 @@
import { SanitizedCollectionConfig } from '../../../../../../../collections/config/types';
export type Props = {
modalSlug: string
modalCollection: SanitizedCollectionConfig
onSave: (json: Record<string, unknown>) => void
}

View File

@@ -0,0 +1,60 @@
@import '../../../../../scss/styles.scss';
.relationship-add-new {
display: flex;
align-items: stretch;
.popup__wrapper {
display: flex;
align-items: stretch;
height: 100%;
}
&__add-button {
@include formInput;
height: 100%;
margin-left: -1px;
display: flex;
padding: 0;
.btn__content,
.btn__label {
display: flex;
}
.btn__content,
.btn__label {
height: 100%;
}
.btn__label {
padding: 0 base(.5);
align-items: center;
}
}
&__relations {
list-style: none;
margin: 0;
padding: 0;
li:not(:last-child) {
margin-bottom: base(.375);
}
}
&__relation-button {
@extend %btn-reset;
cursor: pointer;
@extend %btn-reset;
display: block;
padding: base(.125) 0;
text-align: center;
width: 100%;
&:hover {
opacity: .7;
}
}
}

View File

@@ -0,0 +1,138 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useModal } from '@faceless-ui/modal';
import Button from '../../../../elements/Button';
import { Props } from './types';
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
import Popup from '../../../../elements/Popup';
import { useRelatedCollections } from './useRelatedCollections';
import { useAuth } from '../../../../utilities/Auth';
import { AddNewRelationModal } from './Modal';
import { useEditDepth } from '../../../../utilities/EditDepth';
import Plus from '../../../../icons/Plus';
import './index.scss';
const baseClass = 'relationship-add-new';
export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, value, setValue, dispatchOptions }) => {
const relatedCollections = useRelatedCollections(relationTo);
const { toggleModal, isModalOpen } = useModal();
const { permissions } = useAuth();
const [hasPermission, setHasPermission] = useState(false);
const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>();
const [popupOpen, setPopupOpen] = useState(false);
const editDepth = useEditDepth();
const modalSlug = `${path}-add-modal-depth-${editDepth}`;
const openModal = useCallback(async (collection: SanitizedCollectionConfig) => {
setModalCollection(collection);
toggleModal(modalSlug);
}, [toggleModal, modalSlug]);
const onSave = useCallback((json) => {
const newValue = Array.isArray(relationTo) ? {
relationTo: modalCollection.slug,
value: json.doc.id,
} : json.doc.id;
dispatchOptions({
type: 'ADD',
hasMultipleRelations: Array.isArray(relationTo),
collection: modalCollection,
docs: [
json.doc,
],
sort: true,
});
if (hasMany) {
setValue([...(Array.isArray(value) ? value : []), newValue]);
} else {
setValue(newValue);
}
setModalCollection(undefined);
toggleModal(modalSlug);
}, [relationTo, modalCollection, hasMany, toggleModal, modalSlug, setValue, value, dispatchOptions]);
const onPopopToggle = useCallback((state) => {
setPopupOpen(state);
}, []);
useEffect(() => {
if (permissions) {
if (relatedCollections.length === 1) {
setHasPermission(permissions.collections[relatedCollections[0].slug].create.permission);
} else {
setHasPermission(relatedCollections.some((collection) => permissions.collections[collection.slug].create.permission));
}
}
}, [permissions, relatedCollections]);
useEffect(() => {
if (!isModalOpen(modalSlug)) {
setModalCollection(undefined);
}
}, [isModalOpen, modalSlug]);
return hasPermission ? (
<div
className={baseClass}
id={`${path}-add-new`}
>
{relatedCollections.length === 1 && (
<Button
className={`${baseClass}__add-button`}
onClick={() => openModal(relatedCollections[0])}
buttonStyle="none"
tooltip={`Add new ${relatedCollections[0].labels.singular}`}
>
<Plus />
</Button>
)}
{relatedCollections.length > 1 && (
<Popup
buttonType="custom"
horizontalAlign="center"
onToggleOpen={onPopopToggle}
button={(
<Button
className={`${baseClass}__add-button`}
buttonStyle="none"
tooltip={popupOpen ? undefined : 'Add new'}
>
<Plus />
</Button>
)}
render={({ close: closePopup }) => (
<ul className={`${baseClass}__relations`}>
{relatedCollections.map((relatedCollection) => {
if (permissions.collections[relatedCollection.slug].create.permission) {
return (
<li key={relatedCollection.slug}>
<button
className={`${baseClass}__relation-button ${baseClass}__relation-button--${relatedCollection.slug}`}
type="button"
onClick={() => { closePopup(); openModal(relatedCollection); }}
>
{relatedCollection.labels.singular}
</button>
</li>
);
}
return null;
})}
</ul>
)}
/>
)}
{modalCollection && (
<AddNewRelationModal
{...{ onSave, modalSlug, modalCollection }}
/>
)}
</div>
) : null;
};

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { Action } from '../types';
export type Props = {
hasMany: boolean
relationTo: string | string[]
path: string
value: unknown
setValue: (value: unknown) => void
dispatchOptions: React.Dispatch<Action>
}

View File

@@ -0,0 +1,13 @@
import { useState } from 'react';
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
import { useConfig } from '../../../../utilities/Config';
export const useRelatedCollections = (relationTo: string | string[]): SanitizedCollectionConfig[] => {
const config = useConfig();
const [relatedCollections] = useState(() => {
const relations = typeof relationTo === 'string' ? [relationTo] : relationTo;
return relations.map((relation) => config.collections.find((collection) => collection.slug === relation));
});
return relatedCollections;
};

View File

@@ -5,18 +5,21 @@
margin-bottom: $baseline;
}
.relationship__error-loading {
border: 1px solid var(--theme-error-500);
min-height: base(2);
padding: base(.5) base(.75);
background-color: var(--theme-error-500);
color: var(--theme-elevation-0);
}
.relationship {
&__wrap {
display: flex;
width: 100%;
.relationship--read-only {
div.react-select {
div.rs__control {
background: var(--theme-elevation-100);
div.react-select {
flex-grow: 1;
}
}
}
&__error-loading {
border: 1px solid var(--theme-error-500);
min-height: base(2);
padding: base(.5) base(.75);
background-color: var(--theme-error-500);
color: var(--theme-elevation-0);
}
}

View File

@@ -15,7 +15,7 @@ import FieldDescription from '../../FieldDescription';
import { relationship } from '../../../../../fields/validations';
import { Where } from '../../../../../types';
import { PaginatedDocs } from '../../../../../mongoose/types';
import { useFormProcessing, useWatchForm } from '../../Form/context';
import { useFormProcessing, useAllFormFields } from '../../Form/context';
import optionsReducer from './optionsReducer';
import { Props, Option, ValueWithRelation, GetResults } from './types';
import { createRelationMap } from './createRelationMap';
@@ -23,6 +23,9 @@ import { useDebouncedCallback } from '../../../../hooks/useDebouncedCallback';
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
import { getFilterOptionsQuery } from '../getFilterOptionsQuery';
import wordBoundariesRegex from '../../../../../utilities/wordBoundariesRegex';
import reduceFieldsToValues from '../../Form/reduceFieldsToValues';
import getSiblingData from '../../Form/getSiblingData';
import { AddNewRelation } from './AddNew';
import './index.scss';
@@ -47,7 +50,7 @@ const Relationship: React.FC<Props> = (props) => {
width,
description,
condition,
isSortable,
isSortable = true,
} = {},
} = props;
@@ -61,7 +64,7 @@ const Relationship: React.FC<Props> = (props) => {
const { id } = useDocumentInfo();
const { user, permissions } = useAuth();
const { getData, getSiblingData } = useWatchForm();
const [fields] = useAllFormFields();
const formProcessing = useFormProcessing();
const hasMultipleRelations = Array.isArray(relationTo);
const [options, dispatchOptions] = useReducer(optionsReducer, required || hasMany ? [] : [{ value: null, label: 'None' }]);
@@ -73,6 +76,7 @@ const Relationship: React.FC<Props> = (props) => {
const [search, setSearch] = useState('');
const [enableWordBoundarySearch, setEnableWordBoundarySearch] = useState(false);
const firstRun = useRef(true);
const fieldsRef = useRef(fields);
const memoizedValidate = useCallback((value, validationOptions) => {
return validate(value, { ...validationOptions, required });
@@ -90,6 +94,13 @@ const Relationship: React.FC<Props> = (props) => {
condition,
});
const getFormData = useCallback(() => {
return [
reduceFieldsToValues(fieldsRef.current, true),
getSiblingData(fieldsRef.current, path),
];
}, [fieldsRef, path]);
const getResults: GetResults = useCallback(async ({
lastFullyLoadedRelation: lastFullyLoadedRelationArg,
lastLoadedPage: lastLoadedPageArg,
@@ -148,13 +159,13 @@ const Relationship: React.FC<Props> = (props) => {
query.where.and.push(optionFilters[relation]);
}
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`);
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, { credentials: 'include' });
if (response.ok) {
const data: PaginatedDocs<unknown> = await response.json();
if (data.docs.length > 0) {
resultsFetched += data.docs.length;
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection, sort });
dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort });
setLastLoadedPage(data.page);
if (!data.nextPage) {
@@ -170,7 +181,7 @@ const Relationship: React.FC<Props> = (props) => {
} else if (response.status === 403) {
setLastFullyLoadedRelation(relations.indexOf(relation));
lastLoadedPageToUse = 1;
dispatchOptions({ type: 'ADD', data: { docs: [] } as PaginatedDocs<unknown>, relation, hasMultipleRelations, collection, sort, ids: relationMap[relation] });
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort, ids: relationMap[relation] });
} else {
setErrorLoading('An error has occurred.');
}
@@ -251,11 +262,11 @@ const Relationship: React.FC<Props> = (props) => {
setSearch(searchArg);
}, [getResults]);
const handleInputChange = (searchArg: string, valueArg: unknown) => {
const handleInputChange = useCallback((searchArg: string, valueArg: unknown) => {
if (search !== searchArg) {
updateSearch(searchArg, valueArg);
}
};
}, [search, updateSearch]);
// ///////////////////////////
// Fetch value options when initialValue changes
@@ -284,13 +295,13 @@ const Relationship: React.FC<Props> = (props) => {
};
if (!errorLoading) {
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`);
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, { credentials: 'include' });
const collection = collections.find((coll) => coll.slug === relation);
if (response.ok) {
const data = await response.json();
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection, sort: true, ids });
dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort: true, ids });
} else if (response.status === 403) {
dispatchOptions({ type: 'ADD', data: { docs: [] } as PaginatedDocs, relation, hasMultipleRelations, collection, sort: true, ids });
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort: true, ids });
}
}
}
@@ -301,20 +312,21 @@ const Relationship: React.FC<Props> = (props) => {
}, [hasMany, hasMultipleRelations, relationTo, initialValue, hasLoadedValueOptions, errorLoading, collections, api, serverURL]);
useEffect(() => {
if (!filterOptions) {
return;
}
if (!filterOptions) return;
const [data, siblingData] = getFormData();
const newOptionFilters = getFilterOptionsQuery(filterOptions, {
id,
data: getData(),
data,
relationTo,
siblingData: getSiblingData(path),
siblingData,
user,
});
if (!equal(newOptionFilters, optionFilters)) {
setOptionFilters(newOptionFilters);
}
}, [relationTo, filterOptions, optionFilters, id, getData, getSiblingData, path, user]);
}, [relationTo, filterOptions, optionFilters, id, getFormData, path, user]);
useEffect(() => {
if (optionFilters || !filterOptions) {
@@ -380,50 +392,57 @@ const Relationship: React.FC<Props> = (props) => {
required={required}
/>
{!errorLoading && (
<ReactSelect
isDisabled={readOnly}
onInputChange={(newSearch) => handleInputChange(newSearch, value)}
onChange={!readOnly ? (selected) => {
if (hasMany) {
setValue(selected ? selected.map((option) => {
if (hasMultipleRelations) {
return {
relationTo: option.relationTo,
value: option.value,
};
}
<div className={`${baseClass}__wrap`}>
<ReactSelect
isDisabled={readOnly}
onInputChange={(newSearch) => handleInputChange(newSearch, value)}
onChange={!readOnly ? (selected) => {
if (hasMany) {
setValue(selected ? selected.map((option) => {
if (hasMultipleRelations) {
return {
relationTo: option.relationTo,
value: option.value,
};
}
return option.value;
}) : null);
} else if (hasMultipleRelations) {
setValue({
relationTo: selected.relationTo,
value: selected.value,
return option.value;
}) : null);
} else if (hasMultipleRelations) {
setValue({
relationTo: selected.relationTo,
value: selected.value,
});
} else {
setValue(selected.value);
}
} : undefined}
onMenuScrollToBottom={() => {
getResults({
lastFullyLoadedRelation,
lastLoadedPage: lastLoadedPage + 1,
search,
value: initialValue,
sort: false,
});
} else {
setValue(selected.value);
}
} : undefined}
onMenuScrollToBottom={() => {
getResults({
lastFullyLoadedRelation,
lastLoadedPage: lastLoadedPage + 1,
search,
value: initialValue,
sort: false,
});
}}
value={valueToRender}
showError={showError}
disabled={formProcessing}
options={options}
isMulti={hasMany}
isSortable={isSortable}
filterOption={enableWordBoundarySearch ? (item, searchFilter) => {
const r = wordBoundariesRegex(searchFilter || '');
return r.test(item.label);
} : undefined}
/>
}}
value={valueToRender}
showError={showError}
disabled={formProcessing}
options={options}
isMulti={hasMany}
isSortable={isSortable}
filterOption={enableWordBoundarySearch ? (item, searchFilter) => {
const r = wordBoundariesRegex(searchFilter || '');
return r.test(item.label);
} : undefined}
/>
{!readOnly && (
<AddNewRelation
{...{ path, hasMany, relationTo, value, setValue, dispatchOptions }}
/>
)}
</div>
)}
{errorLoading && (
<div className={`${baseClass}__error-loading`}>

View File

@@ -29,7 +29,8 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
}
case 'ADD': {
const { hasMultipleRelations, collection, relation, data, sort, ids = [] } = action;
const { hasMultipleRelations, collection, docs, sort, ids = [] } = action;
const relation = collection.slug;
const labelKey = collection.admin.useAsTitle || 'id';
@@ -38,20 +39,19 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
if (!hasMultipleRelations) {
const options = [
...state,
...data.docs.reduce((docs, doc) => {
...docs.reduce((docOptions, doc) => {
if (loadedIDs.indexOf(doc.id) === -1) {
loadedIDs.push(doc.id);
return [
...docs,
...docOptions,
{
label: doc[labelKey] || `Untitled - ID: ${doc.id}`,
value: doc.id,
},
];
}
return docs;
},
[]),
return docOptions;
}, []),
];
ids.forEach((id) => {
@@ -69,12 +69,12 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
const newOptions = [...state];
const optionsToAddTo = newOptions.find((optionGroup) => optionGroup.label === collection.labels.plural);
const newSubOptions = data.docs.reduce((docs, doc) => {
const newSubOptions = docs.reduce((docSubOptions, doc) => {
if (loadedIDs.indexOf(doc.id) === -1) {
loadedIDs.push(doc.id);
return [
...docs,
...docSubOptions,
{
label: doc[labelKey] || `Untitled - ID: ${doc.id}`,
relationTo: relation,
@@ -83,7 +83,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
];
}
return docs;
return docSubOptions;
}, []);
ids.forEach((id) => {

View File

@@ -1,5 +1,4 @@
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { PaginatedDocs } from '../../../../../mongoose/types';
import { RelationshipField } from '../../../../../fields/config/types';
export type Props = Omit<RelationshipField, 'type'> & {
@@ -19,8 +18,7 @@ type CLEAR = {
type ADD = {
type: 'ADD'
data: PaginatedDocs<any>
relation: string
docs: any[]
hasMultipleRelations: boolean
collection: SanitizedCollectionConfig
sort?: boolean

View File

@@ -70,6 +70,7 @@ const RichText: React.FC<Props> = (props) => {
const [enabledLeaves, setEnabledLeaves] = useState({});
const [initialValueKey, setInitialValueKey] = useState('');
const editorRef = useRef(null);
const toolbarRef = useRef(null);
const renderElement = useCallback(({ attributes, children, element }) => {
const matchedElement = enabledElements[element?.type];
@@ -93,22 +94,27 @@ const RichText: React.FC<Props> = (props) => {
}, [enabledElements, path, props]);
const renderLeaf = useCallback(({ attributes, children, leaf }) => {
const matchedLeafName = Object.keys(enabledLeaves).find((leafName) => leaf[leafName]);
const matchedLeaves = Object.entries(enabledLeaves).filter(([leafName]) => leaf[leafName]);
if (enabledLeaves[matchedLeafName]?.Leaf) {
const { Leaf } = enabledLeaves[matchedLeafName];
if (matchedLeaves.length > 0) {
return matchedLeaves.reduce((result, [leafName], i) => {
if (enabledLeaves[leafName]?.Leaf) {
const Leaf = enabledLeaves[leafName]?.Leaf;
return (
<Leaf
key={i}
leaf={leaf}
path={path}
fieldProps={props}
editorRef={editorRef}
>
{result}
</Leaf>
);
}
return (
<Leaf
attributes={attributes}
leaf={leaf}
path={path}
fieldProps={props}
editorRef={editorRef}
>
{children}
</Leaf>
);
return result;
}, <span {...attributes}>{children}</span>);
}
return (
@@ -176,6 +182,31 @@ const RichText: React.FC<Props> = (props) => {
setInitialValueKey(JSON.stringify(initialValue || ''));
}, [initialValue]);
useEffect(() => {
function setClickableState(clickState: 'disabled' | 'enabled') {
const selectors = 'button, a, [role="button"]';
const toolbarButtons: (HTMLButtonElement | HTMLAnchorElement)[] = toolbarRef.current?.querySelectorAll(selectors);
const editorButtons: (HTMLButtonElement | HTMLAnchorElement)[] = editorRef.current?.querySelectorAll(selectors);
[...(toolbarButtons || []), ...(editorButtons || [])].forEach((child) => {
const isButton = child.tagName === 'BUTTON';
const isDisabling = clickState === 'disabled';
child.setAttribute('tabIndex', isDisabling ? '-1' : '0');
if (isButton) child.setAttribute('disabled', isDisabling ? 'disabled' : null);
});
}
if (loaded && readOnly) {
setClickableState('disabled');
}
return () => {
if (loaded && readOnly) {
setClickableState('enabled');
}
};
}, [loaded, readOnly]);
if (!loaded) {
return null;
}
@@ -215,13 +246,16 @@ const RichText: React.FC<Props> = (props) => {
editor={editor}
value={valueToRender as any[]}
onChange={(val) => {
if (val !== defaultValue && val !== value) {
if (!readOnly && val !== defaultValue && val !== value) {
setValue(val);
}
}}
>
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__toolbar`}>
<div
className={`${baseClass}__toolbar`}
ref={toolbarRef}
>
<div className={`${baseClass}__toolbar-wrap`}>
{elements.map((element, i) => {
let elementName: string;

View File

@@ -1,7 +1,7 @@
import React, { Fragment, useState, useEffect } from 'react';
import { useConfig } from '../../../../../../../utilities/Config';
import { useAuth } from '../../../../../../../utilities/Auth';
import { useWatchForm } from '../../../../../../Form/context';
import { useFormFields } from '../../../../../../Form/context';
import Relationship from '../../../../../Relationship';
import Select from '../../../../../Select';
@@ -24,9 +24,7 @@ const RelationshipFields = () => {
const { permissions } = useAuth();
const [options, setOptions] = useState(() => createOptions(collections, permissions));
const { getData } = useWatchForm();
const { relationTo } = getData();
const relationTo = useFormFields<string>(([fields]) => fields.relationTo?.value as string);
useEffect(() => {
setOptions(createOptions(collections, permissions));

View File

@@ -42,7 +42,7 @@ const insertUpload = (editor, { value, relationTo }) => {
};
const UploadButton: React.FC<{ path: string }> = ({ path }) => {
const { toggleModal, modalState } = useModal();
const { toggleModal, isModalOpen } = useModal();
const editor = useSlate();
const { serverURL, routes: { api }, collections } = useConfig();
const [availableCollections] = useState(() => collections.filter(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
@@ -65,7 +65,7 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
const modalSlug = `${path}-add-upload`;
const moreThanOneAvailableCollection = availableCollections.length > 1;
const isOpen = modalState[modalSlug]?.isOpen;
const isOpen = isModalOpen(modalSlug);
// If modal is open, get active page of upload gallery
const apiURL = isOpen ? `${serverURL}${api}/${modalCollection.slug}` : null;

View File

@@ -3,6 +3,7 @@
.rich-text {
margin-bottom: base(2);
display: flex;
isolation: isolate;
&__toolbar {
@include blur-bg(var(--theme-elevation-0));
@@ -103,6 +104,29 @@
.rich-text__editor {
background-color: var(--theme-elevation-150);
padding: base(.5);
.popup button {
display: none;
}
}
.rich-text__toolbar {
pointer-events: none;
position: relative;
top: 0;
&::after {
content: ' ';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--theme-elevation-150);
opacity: .85;
z-index: 2;
backdrop-filter: unset;
}
}
}

View File

@@ -16,6 +16,7 @@ const Row: React.FC<Props> = (props) => {
readOnly,
className,
},
indexPath
} = props;
const classes = [
@@ -28,8 +29,9 @@ const Row: React.FC<Props> = (props) => {
<RenderFields
readOnly={readOnly}
className={classes}
permissions={permissions?.fields}
permissions={permissions}
fieldTypes={fieldTypes}
indexPath={indexPath}
fieldSchema={fields.map((field) => ({
...field,
path: getFieldPath(path, field),

View File

@@ -6,4 +6,5 @@ export type Props = Omit<RowField, 'type'> & {
path?: string
fieldTypes: FieldTypes
permissions: FieldPermissions
indexPath: string
}

View File

@@ -3,12 +3,4 @@
.field-type.select {
position: relative;
margin-bottom: $baseline;
}
.select--read-only {
div.react-select {
div.rs__control {
background: var(--theme-elevation-100);
}
}
}
}

View File

@@ -34,7 +34,7 @@ const Select: React.FC<Props> = (props) => {
description,
isClearable,
condition,
isSortable
isSortable = true,
} = {},
} = props;
@@ -95,6 +95,7 @@ const Select: React.FC<Props> = (props) => {
showError={showError}
errorMessage={errorMessage}
required={required}
readOnly={readOnly}
description={description}
style={style}
className={className}

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import RenderFields from '../../RenderFields';
import withCondition from '../../withCondition';
import { Props } from './types';
@@ -7,6 +7,9 @@ import FieldDescription from '../../FieldDescription';
import toKebabCase from '../../../../../utilities/toKebabCase';
import { useCollapsible } from '../../../elements/Collapsible/provider';
import { TabsProvider } from './provider';
import { usePreferences } from '../../../utilities/Preferences';
import { DocumentPreferences } from '../../../../../preferences/types';
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
import './index.scss';
@@ -18,16 +21,57 @@ const TabsField: React.FC<Props> = (props) => {
fieldTypes,
path,
permissions,
indexPath,
admin: {
readOnly,
className,
},
} = props;
const isWithinCollapsible = useCollapsible();
const [active, setActive] = useState(0);
const { getPreference, setPreference } = usePreferences();
const { preferencesKey } = useDocumentInfo();
const activeTab = tabs[active];
const isWithinCollapsible = useCollapsible();
const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
const tabsPrefKey = `tabs-${indexPath}`;
useEffect(() => {
const getInitialPref = async () => {
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey);
const initialIndex = path ? existingPreferences?.fields?.[path]?.tabIndex : existingPreferences?.fields?.[tabsPrefKey]?.tabIndex;
setActiveTabIndex(initialIndex || 0)
}
getInitialPref();
}, [path, indexPath])
const handleTabChange = useCallback(async (incomingTabIndex: number) => {
setActiveTabIndex(incomingTabIndex)
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey);
setPreference(preferencesKey, {
...existingPreferences,
...path ? {
fields: {
...existingPreferences?.fields || {},
[path]: {
...existingPreferences?.fields?.[path],
tabIndex: incomingTabIndex,
},
},
} : {
fields: {
...existingPreferences?.fields,
[tabsPrefKey]: {
...existingPreferences?.fields?.[tabsPrefKey],
tabIndex: incomingTabIndex,
}
},
}
});
}, [indexPath, preferencesKey, getPreference, setPreference, path])
const activeTabConfig = tabs[activeTabIndex];
return (
<div className={[
@@ -39,16 +83,18 @@ const TabsField: React.FC<Props> = (props) => {
<TabsProvider>
<div className={`${baseClass}__tabs-wrap`}>
<div className={`${baseClass}__tabs`}>
{tabs.map((tab, i) => {
{tabs.map((tab, tabIndex) => {
return (
<button
key={i}
key={tabIndex}
type="button"
className={[
`${baseClass}__tab-button`,
active === i && `${baseClass}__tab-button--active`,
activeTabIndex === tabIndex && `${baseClass}__tab-button--active`,
].filter(Boolean).join(' ')}
onClick={() => setActive(i)}
onClick={() => {
handleTabChange(tabIndex)
}}
>
{tab.label ? tab.label : (tabHasName(tab) && tab.name)}
</button>
@@ -57,26 +103,27 @@ const TabsField: React.FC<Props> = (props) => {
</div>
</div>
<div className={`${baseClass}__content-wrap`}>
{activeTab && (
{activeTabConfig && (
<div className={[
`${baseClass}__tab`,
`${baseClass}__tab-${toKebabCase(activeTab.label)}`,
`${baseClass}__tab-${toKebabCase(activeTabConfig.label)}`,
].join(' ')}
>
<FieldDescription
className={`${baseClass}__description`}
description={activeTab.description}
description={activeTabConfig.description}
/>
<RenderFields
key={String(activeTab.label)}
key={String(activeTabConfig.label)}
forceRender
readOnly={readOnly}
permissions={permissions?.fields}
permissions={tabHasName(activeTabConfig) ? permissions[activeTabConfig.name].fields : permissions}
fieldTypes={fieldTypes}
fieldSchema={activeTab.fields.map((field) => ({
fieldSchema={activeTabConfig.fields.map((field) => ({
...field,
path: `${path ? `${path}.` : ''}${tabHasName(activeTab) ? `${activeTab.name}.` : ''}${fieldAffectsData(field) ? field.name : ''}`,
path: `${path ? `${path}.` : ''}${tabHasName(activeTabConfig) ? `${activeTabConfig.name}.` : ''}${fieldAffectsData(field) ? field.name : ''}`,
}))}
indexPath={indexPath}
/>
</div>
)}

View File

@@ -6,4 +6,5 @@ export type Props = Omit<TabsField, 'type'> & {
path?: string
fieldTypes: FieldTypes
permissions: FieldPermissions
indexPath: string
}

View File

@@ -35,7 +35,6 @@ const Text: React.FC<Props> = (props) => {
const field = useField<string>({
path,
validate: memoizedValidate,
enableDebouncedValue: true,
condition,
});

View File

@@ -66,15 +66,24 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
label={label}
required={required}
/>
<textarea
id={`field-${path.replace(/\./gi, '__')}`}
value={value || ''}
onChange={onChange}
disabled={readOnly}
placeholder={placeholder}
name={path}
rows={rows}
/>
<div className="textarea-outer">
<div className="textarea-inner">
<div
className="textarea-clone"
data-value={value || placeholder || ''}
/>
<textarea
className="textarea-element"
id={`field-${path.replace(/\./gi, '__')}`}
value={value || ''}
onChange={onChange}
disabled={readOnly}
placeholder={placeholder}
name={path}
rows={rows}
/>
</div>
</div>
<FieldDescription
value={value}
description={description}

View File

@@ -1,13 +1,22 @@
@import '../../../../scss/styles.scss';
@import "../../../../scss/styles.scss";
.field-type.textarea {
position: relative;
margin-bottom: $baseline;
textarea {
.textarea-outer {
@include formInput();
resize: vertical;
height: auto;
min-height: base(3);
// Scrollbar for giant content
max-height: 90vh;
overflow: auto;
@include mid-break {
max-height: 60vh;
}
}
&.error {
@@ -15,4 +24,65 @@
background-color: var(--theme-error-200);
}
}
// This element takes exactly the same dimensions as the clone
.textarea-inner {
display: block;
position: relative;
line-height: inherit;
flex-grow: 1;
box-sizing: border-box;
background: none;
outline: none;
}
// Unstyle the textarea, the border is rendered on .textarea-outer
.textarea-element {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: inherit;
padding: inherit;
font: inherit;
line-height: inherit;
color: inherit;
background: none;
box-sizing: border-box;
overflow: auto;
resize: none;
outline: none;
text-transform: inherit;
&::-webkit-scrollbar {
display: none;
}
}
// Clone of textarea with same height
.textarea-clone {
vertical-align: top;
display: inline-block;
flex-grow: 1;
white-space: pre;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre-wrap;
pointer-events: none;
}
.textarea-clone::before {
content: attr(data-value) " ";
visibility: hidden;
opacity: 0;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.textarea-clone::after {
content: attr(data-after);
opacity: 0.5;
}
}

View File

@@ -42,7 +42,6 @@ const Textarea: React.FC<Props> = (props) => {
} = useField({
path,
validate: memoizedValidate,
enableDebouncedValue: true,
condition,
});

View File

@@ -11,6 +11,7 @@ import { FieldTypes } from '..';
import AddModal from './Add';
import SelectExistingModal from './SelectExisting';
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { useEditDepth, EditDepthContext } from '../../../utilities/EditDepth';
import './index.scss';
@@ -58,13 +59,15 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
filterOptions,
} = props;
const { toggleModal } = useModal();
const { toggleModal, modalState } = useModal();
const editDepth = useEditDepth();
const addModalSlug = `${path}-add`;
const selectExistingModalSlug = `${path}-select-existing`;
const addModalSlug = `${path}-add-depth-${editDepth}`;
const selectExistingModalSlug = `${path}-select-existing-depth-${editDepth}`;
const [file, setFile] = useState(undefined);
const [missingFile, setMissingFile] = useState(false);
const [modalToRender, setModalToRender] = useState<string>();
const classes = [
'field-type',
@@ -77,7 +80,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
useEffect(() => {
if (typeof value === 'string' && value !== '') {
const fetchFile = async () => {
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`);
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`, { credentials: 'include' });
if (response.ok) {
const json = await response.json();
setFile(json);
@@ -98,6 +101,12 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
serverURL,
]);
useEffect(() => {
if (!modalState[addModalSlug]?.isOpen && !modalState[selectExistingModalSlug]?.isOpen) {
setModalToRender(undefined);
}
}, [modalState, addModalSlug, selectExistingModalSlug]);
return (
<div
className={classes}
@@ -132,6 +141,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
buttonStyle="secondary"
onClick={() => {
toggleModal(addModalSlug);
setModalToRender(addModalSlug);
}}
>
Upload new
@@ -142,33 +152,40 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
buttonStyle="secondary"
onClick={() => {
toggleModal(selectExistingModalSlug);
setModalToRender(selectExistingModalSlug);
}}
>
Choose from existing
</Button>
</div>
)}
<AddModal
{...{
collection,
slug: addModalSlug,
fieldTypes,
setValue: (e) => {
setMissingFile(false);
onChange(e);
},
}}
/>
<SelectExistingModal
{...{
collection,
slug: selectExistingModalSlug,
setValue: onChange,
addModalSlug,
filterOptions,
path,
}}
/>
<EditDepthContext.Provider value={editDepth + 1}>
{modalToRender === addModalSlug && (
<AddModal
{...{
collection,
slug: addModalSlug,
fieldTypes,
setValue: (e) => {
setMissingFile(false);
onChange(e);
},
}}
/>
)}
{modalToRender === selectExistingModalSlug && (
<SelectExistingModal
{...{
collection,
slug: selectExistingModalSlug,
setValue: onChange,
addModalSlug,
filterOptions,
path,
}}
/>
)}
</EditDepthContext.Provider>
<FieldDescription
value={file}
description={description}

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