Compare commits

...

307 Commits

Author SHA1 Message Date
James
3feec809e6 0.0.82 2020-09-23 20:15:45 -04:00
James
8dd3298f9a performance improvements 2020-09-23 20:14:27 -04:00
James
4c11134141 0.0.81 2020-09-23 12:31:08 -04:00
James
45c824c042 allows entire client folder to be parsed by babel/register 2020-09-23 12:31:02 -04:00
James
181d27e33d 0.0.80 2020-09-23 12:20:52 -04:00
James
0e225e1ab1 properly runs babel/register against @payloadcms/payload as a node module 2020-09-23 12:20:44 -04:00
Jarrod Flesch
5676f22ede adds h2-h6 to the rich text editor and style tweaks 2020-09-23 11:22:17 -04:00
James
e9cc178ed7 fixes bug with PurpleBackground demo 2020-09-23 10:04:37 -04:00
James
09a3c859c1 finishes RichText PoC 2020-09-23 10:02:48 -04:00
James
628d68ed2d ensures tests work with new babel config 2020-09-21 21:58:16 -04:00
James
fe99952561 implements babel transpilation of config, removes customComponents logic 2020-09-21 17:31:19 -04:00
James
9c28086ba4 preloads all custom elements and leaves within RichText 2020-09-20 19:01:34 -04:00
James
f8c225316b WIP richText refactor 2020-09-20 18:29:12 -04:00
James
df1f381354 Merge branch 'master' of github.com:keen-studio/payload 2020-09-19 19:32:04 -04:00
James
939a2923d3 implements an easier pattern of pointing to files within config 2020-09-19 19:31:04 -04:00
Elliot DeNolf
0e15f8376c underscore prefix verification fields 2020-09-19 15:03:47 -04:00
Elliot DeNolf
c9d9d5269d make verified hidden 2020-09-19 14:53:06 -04:00
Elliot DeNolf
97f8f2b749 Merge pull request #389 from trouble/verify-jwt-and-apikey
check verification for jwt and apikey
2020-09-19 14:51:38 -04:00
Elliot DeNolf
ea21480230 check verification for jwt and apikey 2020-09-19 14:49:19 -04:00
Elliot DeNolf
2918bbeeca Merge pull request #388 from trouble/verified-login
only allow verified accounts to login
2020-09-19 14:32:08 -04:00
Elliot DeNolf
ac1aea23dc only allow verified accounts to login 2020-09-19 14:29:19 -04:00
Elliot DeNolf
d478bd0e34 Merge pull request #387 from trouble/email-verification 2020-09-19 13:41:25 -04:00
Elliot DeNolf
1695e4e685 implement verification of email against db 2020-09-19 13:39:51 -04:00
James
20bf4680db 0.0.79 2020-09-19 12:15:53 -04:00
James
4f3dd59d25 removes server-config babel plugin in favor of manually blacklisting server modules 2020-09-19 12:15:24 -04:00
James
dde3a35b16 converts radio graphql types to enum 2020-09-19 08:33:03 -04:00
James
a964860e55 0.0.78 2020-09-17 09:19:12 -04:00
James
41dcdabe9c Merge branch 'master' of github.com:keen-studio/payload 2020-09-17 09:19:02 -04:00
James
8ad96452a0 additional field sanitization 2020-09-17 09:18:53 -04:00
Elliot DeNolf
9f426913a2 export all interfaces 2020-09-16 22:06:12 -04:00
Elliot DeNolf
e8376a5662 Merge pull request #386 from trouble/typescript-support 2020-09-16 21:53:17 -04:00
Elliot DeNolf
08dcd7f8b2 better typing for payload function responses 2020-09-16 21:51:34 -04:00
Elliot DeNolf
03e1b730b5 type remaining fields in payload config 2020-09-16 21:08:14 -04:00
James
8252c409d4 0.0.77 2020-09-16 17:22:02 -04:00
James
70ae5b1a19 fixes bug with Relationship field needlessly fetchinig options 2020-09-16 17:21:53 -04:00
James
0d42060882 0.0.76 2020-09-16 13:56:20 -04:00
James
e35b212781 builds relationship whereInput graphQL type 2020-09-16 13:56:02 -04:00
Elliot DeNolf
08731594cb implement payload types module 2020-09-16 12:04:42 -04:00
Elliot DeNolf
c439297223 wip: implement sendVerificationEmail 2020-09-16 10:47:21 -04:00
James
88d80aec55 0.0.75 2020-09-16 09:05:10 -04:00
James
6f6289a069 allows for proper extension of graphql queries / mutations 2020-09-16 09:04:50 -04:00
Elliot DeNolf
af65787862 implement basic typescript support 2020-09-16 00:25:12 -04:00
Elliot DeNolf
a39cec2b76 use crypto for verification token 2020-09-15 14:26:55 -04:00
Elliot DeNolf
f20feae4d9 conditionally add email verification fields to schema 2020-09-15 14:20:28 -04:00
James
2d1169ac66 removes leftover reference to auth update 2020-09-15 11:40:43 -04:00
James
966dd7a40f allows for user override in local operations 2020-09-15 08:57:49 -04:00
James
eee835696c adds control over overrideAccess in findByID 2020-09-14 21:53:11 -04:00
James
e95edbd3e4 fixes css imports like an adult 2020-09-14 21:25:11 -04:00
James
b462fe5f77 stringifies globals before sending, fixes async bug 2020-09-14 21:21:13 -04:00
James
a1865ed5fb Merge branch 'master' of github.com:keen-studio/payload 2020-09-14 21:04:44 -04:00
James
209d812239 fixes bug with relationship population in graphql resolver 2020-09-14 21:04:17 -04:00
James
4e3cc2bc05 comments out breaking graphql errors 2020-09-14 21:04:02 -04:00
James
f5731f2fc8 moves position of beforeRead operation 2020-09-14 21:03:50 -04:00
James
29b608d6f8 properly imports css in DatePicker 2020-09-14 21:03:06 -04:00
James
a10dd738c0 removes old lines in gitignore 2020-09-14 21:02:52 -04:00
James
0fb96ff3e9 fixes bug with loading css in DatePicker 2020-09-14 21:02:40 -04:00
Elliot DeNolf
55488c5c2e Merge pull request #384 from trouble/global-update-access
fix global update's execution of access
2020-09-14 20:50:43 -04:00
Elliot DeNolf
5345eb993a fix global update's execution of access 2020-09-14 17:34:56 -04:00
Elliot DeNolf
8f67ee4ac3 Merge pull request #382 from trouble/local-crud-globals 2020-09-14 17:03:21 -04:00
James
9f8b1d9521 fixes bug with ensuring hasMany relationships have initial values 2020-09-13 12:24:04 -04:00
James
d4c4418a3e 0.0.74 2020-09-13 12:06:14 -04:00
James
bb5f747052 flattens create / register, update / authUpdate 2020-09-13 12:06:02 -04:00
Elliot DeNolf
f2f8ffbbb2 implement global update 2020-09-13 08:40:28 -04:00
James
fab4c0ed76 fixes accidentally running hooks for non-populated subdocs, waits for hooks before running population 2020-09-12 22:56:15 -04:00
James
7b3cefd4ef fixes bug with zero-based radio values 2020-09-12 22:55:18 -04:00
James
ad483da837 changes timestamps to opt-out 2020-09-12 22:55:05 -04:00
James
01d49ccd36 removes console log 2020-09-12 17:48:46 -04:00
James
7ceadc15a3 just wasted about a half hour of time on this 2020-09-12 17:45:55 -04:00
James
3caf81d9f7 redirects to NotFound properly when editing a document that does not exist 2020-09-12 16:42:04 -04:00
James
dd6740e109 only shows LeaveWithoutSaving if user is logged in 2020-09-12 16:36:23 -04:00
James
28e4cf6726 fixes bug in code field 2020-09-12 16:33:14 -04:00
James
95e38a568f adds Code field 2020-09-12 16:24:03 -04:00
James
43f1f41179 0.0.73 2020-09-11 15:12:17 -04:00
James
698a9ead04 fixes bug with field sanitization and JWT graphql type 2020-09-11 15:12:02 -04:00
James
bd3f628d55 Merge branch 'master' of github.com:keen-studio/payload 2020-09-11 14:11:51 -04:00
James
14d702ff8d improves graphql name formatting 2020-09-11 14:11:33 -04:00
James
dd5acae76f only sets nullable gql type if no admin condition 2020-09-11 14:11:23 -04:00
James
9de51bbf02 fixes gql in relationships with many relations 2020-09-11 14:11:10 -04:00
James
9c44a159b9 formats radio values as strings within validation 2020-09-11 12:50:41 -04:00
James
43e3162c7d fixes bug within FileDetails when no sizes present 2020-09-11 12:49:44 -04:00
Elliot DeNolf
2b81e887ad implement global find 2020-09-11 11:33:26 -04:00
Elliot DeNolf
bfd492b7ed Merge pull request #377 from trouble/validate-relationships 2020-09-10 21:49:46 -04:00
Elliot DeNolf
bf452c23bc Add tests for sanitizeFields 2020-09-10 21:47:55 -04:00
Elliot DeNolf
da3495cb4b fix MissingFieldType missing export 2020-09-10 21:18:01 -04:00
Elliot DeNolf
899a158843 fix parameter ordering for sanitizeFields calls 2020-09-10 21:17:46 -04:00
Elliot DeNolf
163daf5816 validate global relationships 2020-09-10 19:35:55 -04:00
Elliot DeNolf
7fbab16b22 validate relationships within blocks, gettin' recursive with it 2020-09-10 19:17:57 -04:00
Elliot DeNolf
17b8335c3c validate relationTo on fields 2020-09-10 18:07:07 -04:00
Elliot DeNolf
93930d1125 Merge pull request #376 from trouble/mongoose-indexing
Disable auto-indexing, index if set to true or if unique
2020-09-10 16:41:54 -04:00
Elliot DeNolf
9cb630f98d Disable auto-indexing, index if set to true or if unique 2020-09-10 13:02:09 -04:00
Jarrod Flesch
f695bd8c1d increases font size on date picker times 2020-09-09 14:25:12 -04:00
Jarrod Flesch
8a8222995f better date validation, date-picker style updates 2020-09-08 15:11:18 -04:00
Jarrod Flesch
ca24263c17 removes duplicate key from babel-loader removeObjectProperties 2020-09-08 13:31:25 -04:00
James
0bf0dd7d5e 0.0.72 2020-09-08 10:47:25 -04:00
James
ef3a70cc38 bumps slate 2020-09-08 10:47:21 -04:00
James
6fe09c8264 fixes bugs within auth / graphQL 2020-09-07 14:47:03 -04:00
James
4616e7fa16 sends fields on mount within useFieldType 2020-09-05 17:44:41 -04:00
James
d99788f6f7 0.0.71 2020-09-05 12:25:29 -04:00
James
8a1bd762ba implements a way to add custom express middleware 2020-09-05 12:23:39 -04:00
James
c760863f8c enables Upload GraphQL field resolver 2020-09-05 12:19:32 -04:00
James
ed5c3b55f1 0.0.70 2020-09-01 18:22:04 -04:00
James
7d0e2820cc implements cookie expiration, streamlines cookie options on collection config level 2020-09-01 18:21:51 -04:00
James
f381b9261a 0.0.69 2020-08-29 16:24:35 -04:00
James
01f170ba73 further revises initialState fixes to Array, Blocks 2020-08-29 16:24:27 -04:00
James
a53e24904f fixes state initialization in Array and Blocks 2020-08-29 12:08:26 -04:00
James
423cdbd63f removes field from state if failing condition 2020-08-29 12:08:12 -04:00
James
e38c0e687f fixes bug in Radio field type 2020-08-29 12:07:32 -04:00
James
9e82f9d02e fixes bugs with GraphQL when no locale is present 2020-08-29 12:07:06 -04:00
James
5f9c48494a fixes bug in refresh resolver 2020-08-29 12:06:12 -04:00
James
155db092c4 adds safety check to getExtractJWT 2020-08-29 12:05:40 -04:00
James
87522ff7d6 0.0.68 2020-08-28 15:07:22 -04:00
James
e6b435e88e fixes bug with forgotPassword 2020-08-28 15:07:16 -04:00
James
8c78a0a773 0.0.67 2020-08-28 13:43:29 -04:00
James
597f7d3075 fixes bugs with columns in List, logs errors within email config, creates local delete operation 2020-08-28 13:43:17 -04:00
James
9da8aca015 Merge branch 'master' of github.com:keen-studio/payload 2020-08-28 11:29:34 -04:00
Elliot DeNolf
13aa1eb13d Merge pull request #367 from trouble/better-error-logging
Handle values other than string in APIError
2020-08-26 23:35:38 -04:00
Elliot DeNolf
918228c9dc Handle values other than string in APIError 2020-08-26 23:14:28 -04:00
James
92a25867ff 0.0.66 2020-08-26 18:34:59 -04:00
James
f698594137 implements lowercasing in forgotPassword 2020-08-26 18:34:39 -04:00
James
c15dff4afb Merge branch 'master' of github.com:keen-studio/payload 2020-08-26 18:31:31 -04:00
James
985db67d48 ensures login is done with lowercase 2020-08-26 18:31:25 -04:00
Elliot DeNolf
30c5eeaff9 Merge pull request #364 from trouble/favicon
Implement favicon and og image
2020-08-25 21:39:06 -04:00
Elliot DeNolf
fd81085b54 Implement favicon and og image 2020-08-25 21:37:17 -04:00
James
9955621f0a 0.0.65 2020-08-25 16:06:27 -04:00
James
599a693f5b fixes bugs with columns in List view 2020-08-25 16:06:21 -04:00
James
da6a9a2ba7 0.0.64 2020-08-25 14:39:33 -04:00
James
7002752744 adds serverURL to CSRF 2020-08-25 14:39:27 -04:00
James
53ed79ecbc code splits heavy field types 2020-08-25 12:21:37 -04:00
James
1e342f8bee further improvements to splitting browser and server code 2020-08-25 11:05:19 -04:00
James
d19576978b implements the removal of server-side code from client bundle 2020-08-25 10:36:34 -04:00
James
af59822510 implements pattern for removing properties from config 2020-08-25 09:44:53 -04:00
James
f45f93ce38 0.0.63 2020-08-24 20:35:35 -04:00
James
0d1ffdcb4b removes webpack bundle analyzer 2020-08-24 20:35:25 -04:00
James
b197d780c1 passes id of document to access operations where required 2020-08-24 17:02:03 -04:00
James
45da1369ca 0.0.62 2020-08-24 12:55:16 -04:00
James
478d1cf5ee removes webpack-node-externals 2020-08-24 12:55:02 -04:00
James
ec0b2c6a5b 0.0.61 2020-08-24 12:19:27 -04:00
James
3a5881cc48 lowercases email on registration, adds node-webpack-externals to dev webpack cfg 2020-08-24 12:19:24 -04:00
Dan Ribbens
13c857f753 Merge pull request #360 from trouble/gql-spec-bug
fix gql random test failures
2020-08-23 18:56:57 -04:00
Dan Ribbens
5091a40e96 fix gql random test failures 2020-08-23 18:53:10 -04:00
Dan Ribbens
1cf45f7fa9 Merge pull request #359 from trouble/select-field-validation
Select field validation
2020-08-23 18:38:30 -04:00
Dan Ribbens
b49b531ce4 select field validation for hasMany fields 2020-08-23 18:35:28 -04:00
James
7553e14a8b merges master 2020-08-23 18:12:53 -04:00
James
cb984f4d54 implements webpack-node-externals 2020-08-23 18:10:47 -04:00
Dan Ribbens
9630c2c3b2 add validation and schema enum to radio field type 2020-08-23 17:15:40 -04:00
Dan Ribbens
8bfc892f7c radio field validation on options 2020-08-23 16:49:51 -04:00
Dan Ribbens
756d275edf select field validation on options 2020-08-23 16:48:01 -04:00
Dan Ribbens
bfe7a139af Merge pull request #357 from trouble/remove-hash-salt-auth-responses
set hide option for salt and hash in auth collections
2020-08-23 16:09:56 -04:00
Dan Ribbens
95352f47ea set hide option for salt and hash in auth collections 2020-08-23 16:07:06 -04:00
Elliot DeNolf
98a9eb8d23 Merge pull request #356 from trouble/custom-index 2020-08-23 07:33:34 -04:00
Elliot DeNolf
9a62c2ab5d Use payload/config alias in Meta 2020-08-23 07:28:59 -04:00
Elliot DeNolf
9c928d497c Implement custom index 2020-08-23 07:26:02 -04:00
Elliot DeNolf
6f777dd09c Merge pull request #355 from trouble/page-titles
Implement page titles based upon active view
2020-08-22 19:59:18 -04:00
Elliot DeNolf
5777868eba Implement page titles based upon active view 2020-08-22 19:53:57 -04:00
Dan Ribbens
063b3b86c8 Merge pull request #354 from trouble/cookie-same-site
add samesite config setting to auth
2020-08-22 00:14:58 -04:00
Dan Ribbens
9ef9cca948 add samesite config setting to auth 2020-08-22 00:08:46 -04:00
James
74ac23dbc6 0.0.60 2020-08-21 17:44:05 -04:00
James
b2145a0a59 fixes Add Upload modal 2020-08-21 17:42:04 -04:00
James
fe307ecfeb fixes inability to remove file from upload field 2020-08-21 17:30:32 -04:00
James
80b99653cf fixes bugs with LeaveWithoutSaving modal 2020-08-21 17:25:51 -04:00
James
75aa92952a 0.0.59 2020-08-21 16:25:36 -04:00
James
3375963449 fixes bugs within Update operation 2020-08-21 16:25:09 -04:00
James
f91c47bb37 fixes bug introduced with csrf 2020-08-21 15:44:03 -04:00
James
60552d9d86 implements potential csrf protection 2020-08-21 15:20:21 -04:00
James
857cf088f6 roughs out pattern for awaiting ensureIndex on all models 2020-08-21 15:19:50 -04:00
James
5c506a1db7 0.0.58 2020-08-20 16:30:31 -04:00
James
b81c7917a9 fixes a bug with block fields not being iterated through in perforFieldOperations, fixes checkboxes being sent as strings 2020-08-20 16:30:01 -04:00
James
5802011eea fixes errorHandler tests 2020-08-20 09:16:50 -04:00
James
c91f4d9e0d 0.0.57 2020-08-19 16:29:48 -04:00
James
b5690cf721 fixes bug with CORS headers 2020-08-19 16:29:35 -04:00
James
aaee4d16d1 0.0.56 2020-08-18 18:05:16 -04:00
James
5148a21769 fixes bug with forgotPassword custom expiration 2020-08-18 18:05:03 -04:00
James
ec461e2440 0.0.55 2020-08-18 17:24:40 -04:00
James
ada2b79054 implements html override in forgotPassword 2020-08-18 17:24:33 -04:00
James
3d652b2c9a 0.0.54 2020-08-18 14:50:21 -04:00
James
63bc00e8da fixese bug in auth update operation 2020-08-18 14:50:07 -04:00
James
8162b1fc93 0.0.53 2020-08-18 13:16:55 -04:00
James
a009387461 fixes bad syntax in login operation 2020-08-18 13:16:51 -04:00
James
8dbf10dc4a 0.0.52 2020-08-18 13:15:07 -04:00
James
954cd9437b Merge branch 'master' of github.com:keen-studio/payload 2020-08-18 13:14:57 -04:00
James
1c532d3719 0.0.51 2020-08-18 13:14:35 -04:00
James
273351103d adds fields within rows and other types to JWT if necessary 2020-08-18 13:14:28 -04:00
Jarrod Flesch
fd15b06773 fix: removes section title input bg, removes right gutter border line on parent hover 2020-08-18 09:15:11 -04:00
James
9efb5e881b removes a bug-producing effect in Checkbox that sets value on mount 2020-08-14 16:45:36 -04:00
James
e674bb6779 0.0.50 2020-08-14 15:51:10 -04:00
James
8df93b23b5 Merge branch 'master' of github.com:keen-studio/payload 2020-08-14 15:51:00 -04:00
James
afc47d92b5 fixes bug with authenticating via API Key 2020-08-14 15:50:50 -04:00
Elliot DeNolf
d47c8a240a Update README 2020-08-14 13:50:21 -04:00
James
9be27927c1 implements a method of overriding resetPasswordExpiration 2020-08-14 13:36:54 -04:00
James
e0c1400ee4 0.0.49 2020-08-14 13:20:15 -04:00
James
eb604a566d merges master 2020-08-14 13:17:57 -04:00
James
0b2add2e1a recursively adds block type discriminators 2020-08-14 13:17:34 -04:00
Elliot DeNolf
1ca046532d Merge pull request #345 from trouble/expose-logger
Expose logger for external use
2020-08-14 13:11:17 -04:00
Elliot DeNolf
c1843170d6 expose logger for external use 2020-08-14 13:08:50 -04:00
James
85bf65e40e fixes breaking GraphQLDate within resolvers due to JSON.stringify 2020-08-14 12:33:42 -04:00
James
2a2494366b 0.0.48 2020-08-14 10:05:01 -04:00
James
b53dbc1a27 adds update, adds way to disable email within forgotPassword operation 2020-08-14 10:04:48 -04:00
James
6c881e3c48 0.0.47 2020-08-13 18:29:23 -04:00
James
17b4251176 adds local forgotPassword, resetPassword, findByID 2020-08-13 18:29:12 -04:00
James
c7411fd347 modifies payload authenticate middleware API and exposes errorHandler 2020-08-13 16:02:19 -04:00
James
f32e7f8f4c 0.0.46 2020-08-13 10:36:02 -04:00
James
2667c6fa98 converts payload to singleton, adds local create 2020-08-13 10:35:25 -04:00
James
46309f33a2 properly sets default values if no value present on update or create 2020-08-13 09:18:02 -04:00
James
fb495d7b04 0.0.45 2020-08-12 16:41:04 -04:00
James
f8840ee8b7 adds login and register local operations 2020-08-12 16:40:51 -04:00
James
3c025377f5 0.0.44 2020-08-11 19:59:47 -04:00
James
a01b308c38 fixes bugs with DatePicker, further refinements to draggable section 2020-08-11 19:59:34 -04:00
James
53c56c537d aesthetic improvements to Group, Array, Blocks 2020-08-11 19:26:34 -04:00
James
a669ca7099 0.0.43 2020-08-11 16:01:12 -04:00
James
258d4cd62e swaps logout to POST instead of GET 2020-08-11 16:00:55 -04:00
James
fe2c1e69e5 0.0.42 2020-08-11 15:51:50 -04:00
James
a77dcaa6c2 implements proper reset password workflow within admin panel and sets cookie properly 2020-08-11 15:51:36 -04:00
James
dfd4f75c98 0.0.41 2020-08-11 14:02:28 -04:00
James
322b726be8 properly threads overrideAccess through register operation 2020-08-11 13:59:31 -04:00
James
2978509034 0.0.40 2020-08-11 12:07:57 -04:00
James
d9c1b41e08 properly replaces https in cookie domains 2020-08-11 12:07:45 -04:00
James
9538af2e92 0.0.39 2020-08-11 11:54:37 -04:00
James
2eb19876ee adds system vars to webpack dotenv 2020-08-11 11:54:28 -04:00
James
74fb5397ac 0.0.38 2020-08-11 10:55:14 -04:00
James
0630c006d5 overrides field level policies properly 2020-08-11 10:52:29 -04:00
James
71a5a525d1 simplifies Dashboard component 2020-08-10 17:37:23 -04:00
James
5ba0313b8f simplifies Default List, removes Create buttons if collection create permission is denied 2020-08-10 17:28:49 -04:00
James
7a775166b2 Merge branch 'master' of github.com:keen-studio/payload 2020-08-10 16:59:34 -04:00
Dan Ribbens
9edf4ae2da Merge pull request #340 from trouble/gitattributes
add .gitattributes
2020-08-06 08:54:49 -04:00
Dan Ribbens
fcef33ad1f add .gitattributes 2020-08-06 08:49:40 -04:00
Elliot DeNolf
cbf18d75a7 Merge pull request #339 from trouble/more-gql-tests 2020-08-05 22:09:56 -04:00
Elliot DeNolf
6f38e5c264 test(graphql): delete localized post 2020-08-05 21:58:46 -04:00
Elliot DeNolf
ead0568a94 test(graphql): read localized post 2020-08-05 21:51:42 -04:00
James
5b5acd7471 0.0.37 2020-07-31 12:12:58 -04:00
James
87e79817f7 fixes errors with client registration process 2020-07-31 12:12:36 -04:00
Elliot DeNolf
2b969b91b9 Merge pull request #338 from trouble/media-tests 2020-07-31 10:21:41 -04:00
Elliot DeNolf
609bf54c45 Fix media test if media dir doesn't exist yet 2020-07-31 10:14:31 -04:00
James
9a104b5d12 0.0.36 2020-07-31 08:12:07 -04:00
James
65eccb129f 0.0.35 2020-07-30 17:40:36 -04:00
Elliot DeNolf
022bf17b59 Assert file doesn't exist on media delete 2020-07-30 14:01:41 -04:00
James
ae7583401e allows for sanitization of string true / false in buildQuery 2020-07-30 13:39:54 -04:00
James
1ca904f164 Merge branch 'master' of github.com:keen-studio/payload 2020-07-30 13:32:15 -04:00
James
bc884e64dd revisions to field styles 2020-07-30 13:32:11 -04:00
Jarrod Flesch
89ceff24fc adjusts Icon rendering, add min height to edit sticky so save button can always be seen 2020-07-30 12:39:49 -04:00
Jarrod Flesch
47b2940358 style tweaks to FieldTypeGutters 2020-07-30 12:31:10 -04:00
Jarrod Flesch
facff24abc Merge pull request #334 from trouble/bugfix/nested-repeaters
bugfix(style): fixes nested fieldTypeGutters positioning
2020-07-30 11:20:18 -04:00
Jarrod Flesch
8f10f14bc4 bugfix(style): fixes nested fieldTypeGutters positioning 2020-07-30 11:19:45 -04:00
James
3c522930ee fixes cases where hooks return undefined 2020-07-30 10:04:17 -04:00
Elliot DeNolf
0da92c20ea Add jest debug current file and adjust Jest timeout 2020-07-30 09:46:52 -04:00
Elliot DeNolf
43db6a491d Add media delete test 2020-07-30 09:46:21 -04:00
James
ae77d68f72 0.0.34 2020-07-30 09:35:19 -04:00
James
0e76d3e79d updates globals to use form initial state, resets field state on initial state change 2020-07-30 09:25:34 -04:00
Elliot DeNolf
f0ae33f780 Add media create test 2020-07-30 08:31:48 -04:00
James
684e23fa2f adds a few safety checks to query building 2020-07-30 08:19:33 -04:00
James
bf2ed1747c 0.0.33 2020-07-30 07:27:06 -04:00
James
90a09e268f passes operation through to field hooks 2020-07-30 07:26:48 -04:00
James
0cc468ad6a fixes useThrottledEffect 2020-07-29 21:19:28 -04:00
James
16b9aab785 modifies create and update hooks to 'change' instead 2020-07-29 19:19:15 -04:00
James
749388e877 merges Group style updates 2020-07-29 13:48:05 -04:00
James
17dfa693ba opens up margin below grouped field types 2020-07-29 13:47:35 -04:00
Jarrod Flesch
2dcb80e681 merges with master 2020-07-29 13:19:47 -04:00
Jarrod Flesch
26ab75a356 Adds more style tweaks to groups
- add flex wrap to rows
- renames RenderFieldGutter to FieldTypeGutter
- removes some misc unused code
2020-07-29 13:14:37 -04:00
James
97c31faea1 waits for hooks to be resolved before validating 2020-07-29 09:34:00 -04:00
James
8b167e9a9e implements pattern for adding additional cell components within List view 2020-07-29 08:10:36 -04:00
James
d9bf8a5fcc 0.0.32 2020-07-28 21:16:48 -04:00
James
7b5dd0ac5f fixes bug with unset checkbox values sending as empty strings 2020-07-28 21:16:34 -04:00
James
210295e704 implements a way to clear relationship field values 2020-07-28 20:32:24 -04:00
James
ed56971de6 merges progress to Group styles 2020-07-28 19:50:48 -04:00
James
298948b203 supports wildcard cors header 2020-07-28 19:46:58 -04:00
James
f5b55ddbb4 fixes z-index within react-select options menu 2020-07-28 19:41:34 -04:00
James
99d083bb09 implements a way to disable duplication, disables readOnly if operation is create 2020-07-28 19:39:38 -04:00
James
455f2ab183 implements readOnly on more fields, fixes bugs with relationship 2020-07-28 19:32:41 -04:00
James
f4127e1820 0.0.31 2020-07-28 16:51:13 -04:00
James
8fa95b7851 allows readOnly to cascade through children 2020-07-28 16:50:54 -04:00
James
aefdabfd7e mysteriously fixes error handler by adding next argument 2020-07-28 14:12:57 -04:00
James
470a33f7f9 ensures payload is passed through local find operation 2020-07-28 14:08:27 -04:00
Jarrod Flesch
d179427d7b removes border-radius on editable title 2020-07-28 09:30:54 -04:00
Jarrod Flesch
599af3f924 adjusts line hover states 2020-07-28 09:24:22 -04:00
Jarrod Flesch
6e383d9005 resolves css conflicts with master 2020-07-28 09:05:45 -04:00
Jarrod Flesch
d74c1e3b93 resolves conflicts and adds top-offset-variable 2020-07-28 01:26:49 -04:00
Jarrod Flesch
c1814cbc3f adjusts the group styles and refactors how the block panels are composed 2020-07-28 01:13:33 -04:00
James
c0fef9bfde 0.0.30 2020-07-27 19:18:43 -04:00
James
1363bec776 improves styling to sticky headers in blocks 2020-07-27 19:08:37 -04:00
James
f3972430f6 merges error-handler-logging 2020-07-27 18:57:08 -04:00
James
998cb656a5 merges rte-crashing 2020-07-27 18:56:29 -04:00
James
1a8228c08b Merge branch 'bugfix/#275' of github.com:keen-studio/payload 2020-07-27 18:55:26 -04:00
James
07f9a06571 Merge branch 'bugfix/#309' of github.com:keen-studio/payload 2020-07-27 18:55:07 -04:00
James
9c8145238c adds pattern for local CRUD 2020-07-27 17:04:54 -04:00
Jarrod Flesch
3445f5bdce sticks the section header to the top of block sections - #309 2020-07-27 16:43:45 -04:00
Jarrod Flesch
8d3573d734 adds scrollbar width and adjusts popup width accordingly - #275 2020-07-27 16:08:53 -04:00
Jarrod Flesch
1fc32fa0bb removes styled-components from package.json and yarn.lock 2020-07-27 15:01:59 -04:00
Jarrod Flesch
255f6e6215 migrate slate-plugins to newest version, dropping peer dep of styled-components 2020-07-27 14:30:15 -04:00
Jarrod Flesch
1aa649782b merges with master 2020-07-27 12:38:26 -04:00
James
16d2f400c4 0.0.29 2020-07-27 12:31:42 -04:00
James
4d99594021 temporarily re-adds styled components as dependency 2020-07-27 12:31:28 -04:00
James
790c85df5a 0.0.28 2020-07-27 12:01:14 -04:00
James
0a98778ec4 Merge branch 'master' of github.com:keen-studio/payload 2020-07-27 12:01:07 -04:00
James
cc4794e5ac implements initial form state 2020-07-27 11:53:00 -04:00
Dan Ribbens
e301efdb7b add payload logger to error handler middleware 2020-07-24 10:32:41 -04:00
Elliot DeNolf
a4cf98c460 Merge pull request #321 from trouble/logger 2020-07-24 09:39:57 -04:00
Elliot DeNolf
c9e37bcef5 Better formatting for logger output 2020-07-24 09:37:49 -04:00
Elliot DeNolf
deaf5cde5f Initial logger implementation with pino 2020-07-23 21:57:11 -04:00
James
d38b8e746c removes unused dependencies 2020-07-23 19:18:24 -04:00
James
1be0890f03 0.0.27 2020-07-23 19:16:16 -04:00
James
d45ff316c0 implements intersection observer to mount fields over time 2020-07-23 19:16:01 -04:00
James
d2055141be performance enhancements and render reductions 2020-07-23 17:40:16 -04:00
James
846589e303 0.0.26 2020-07-23 12:18:19 -04:00
James
8efc03b325 0.0.25 2020-07-23 12:18:13 -04:00
James
28d8c10a3e Revert "0.0.24"
This reverts commit f42b2fc670.
2020-07-23 12:15:05 -04:00
Jarrod Flesch
cddba8d9e7 removes internal state from the RTE, updates to the form logic fixed previous issues 2020-07-03 09:27:58 -04:00
Jarrod Flesch
fa02f00503 Merge branch 'master' of github.com:trouble/payload into bugfix/#258-rte-crashing 2020-07-02 17:23:13 -04:00
Jarrod Flesch
9a095f574b adds ExitBreakPlugin, issue has to do with SoftBreakPlugin 2020-07-02 11:44:18 -04:00
315 changed files with 9437 additions and 5258 deletions

View File

@@ -11,5 +11,6 @@ module.exports = {
]
}
],
'no-underscore-dangle': 'off',
},
};

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto eol=lf

5
.gitignore vendored
View File

@@ -218,11 +218,6 @@ $RECYCLE.BIN/
# End of https://www.gitignore.io/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
# Ignores compiled CSS
src/**/*.css
demo**/*.css
dist
# Ignore all uploads
demo/upload
demo/media

25
.vscode/launch.json vendored
View File

@@ -18,7 +18,30 @@
},
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
"port": 9229,
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Debug Jest Test - Current File",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/.bin/jest",
"${fileBasename}",
"--runInBand"
],
"env": {
"PAYLOAD_CONFIG_PATH": "demo/payload.config.js"
},
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229,
"skipFiles": [
"<node_internals>/**"
]
},
{
"type": "node",

View File

@@ -1,8 +1,21 @@
# Payload
Headless CMS
Headless CMS and application framework
*TODO: More on why to use it and some features*
## Installation
`yarn add @payloadcms/payload` or `npm install @payloadcms/payload`
## Usage
## Development
*TODO: Show basic usage and link to docs*
## Contributing
*TODO: Create Contributing.md*
## License
*TODO: Create License.md*

3
admin/components.js Normal file
View File

@@ -0,0 +1,3 @@
export { default as Popup } from '../src/client/components/elements/Popup';
export { default as MinimalTemplate } from '../src/client/components/templates/Minimal';

1
admin/elements.js Normal file
View File

@@ -0,0 +1 @@
export { default as Button } from '../src/client/components/elements/Button';

6
admin/forms.js Normal file
View File

@@ -0,0 +1,6 @@
export { default as Form } from '../src/client/components/forms/Form';
export { default as Text } from '../src/client/components/forms/field-types/Text';
export { default as Select } from '../src/client/components/forms/field-types/Select';
export { default as Checkbox } from '../src/client/components/forms/field-types/Checkbox';
export { default as Submit } from '../src/client/components/forms/Submit';
export { default as reduceFieldsToValues } from '../src/client/components/forms/Form/reduceFieldsToValues';

1
admin/icons.js Normal file
View File

@@ -0,0 +1 @@
export { default as X } from '../src/client/components/icons/X';

4
admin/rich-text.js Normal file
View File

@@ -0,0 +1,4 @@
export { default as LeafButton } from '../src/client/components/forms/field-types/RichText/LeafButton';
export { default as ElementButton } from '../src/client/components/forms/field-types/RichText/ElementButton';
export { default as toggleElement } from '../src/client/components/forms/field-types/RichText/toggleElement';

1
admin/styles.scss Normal file
View File

@@ -0,0 +1 @@
@import '../src/client/scss/styles.scss';

21
babel.config.js Normal file
View File

@@ -0,0 +1,21 @@
module.exports = {
presets: [
[
require.resolve('@babel/preset-env'),
{
modules: 'cjs',
targets: [
'defaults',
'not IE 11',
'not IE_Mob 11',
],
},
],
require.resolve('@babel/preset-react'),
],
plugins: [
require.resolve('@babel/plugin-proposal-class-properties'),
require.resolve('@babel/plugin-proposal-optional-chaining'),
require.resolve('babel-plugin-add-module-exports'),
],
};

View File

@@ -1,9 +0,0 @@
import PageList from '../collections/Page/components/List';
const components = {
pages: {
List: PageList,
},
};
export default components;

View File

@@ -0,0 +1,104 @@
import React, { Fragment, useCallback } from 'react';
import { Modal, useModal } from '@faceless-ui/modal';
import { Transforms } from 'slate';
import { useSlate } from 'slate-react';
import { MinimalTemplate } from '../../../../../../../admin/components';
import { ElementButton } from '../../../../../../../admin/rich-text';
import { X } from '../../../../../../../admin/icons';
import { Button } from '../../../../../../../admin/elements';
import { Form, Text, Checkbox, Select, Submit, reduceFieldsToValues } from '../../../../../../../admin/forms';
import './index.scss';
const modalSlug = 'add-button';
const baseClass = 'button-rich-text-button';
const insertButton = (editor, { url, label, style, newTab = false }) => {
const text = { text: label };
const button = {
type: 'button',
url,
style,
newTab,
children: [
text,
],
};
Transforms.insertNodes(editor, button);
};
const ButtonButton = () => {
const { open, closeAll } = useModal();
const editor = useSlate();
const handleAddButton = useCallback((fields) => {
const data = reduceFieldsToValues(fields);
insertButton(editor, data);
closeAll();
}, [editor, closeAll]);
return (
<Fragment>
<ElementButton
className={baseClass}
format="button"
onClick={() => open(modalSlug)}
>
Button
</ElementButton>
<Modal
slug={modalSlug}
className={`${baseClass}__modal`}
>
<MinimalTemplate>
<header className={`${baseClass}__header`}>
<h3>Add button</h3>
<Button
buttonStyle="none"
onClick={closeAll}
>
<X />
</Button>
</header>
<Form onSubmit={handleAddButton}>
<Text
label="Label"
name="label"
required
/>
<Text
label="URL"
name="URL"
required
/>
<Select
label="Style"
name="style"
options={[
{
label: 'Primary',
value: 'primary',
},
{
label: 'Secondary',
value: 'secondary',
},
]}
/>
<Checkbox
label="Open in new tab"
name="newTab"
/>
<Submit>
Add button
</Submit>
</Form>
</MinimalTemplate>
</Modal>
</Fragment>
);
};
export default ButtonButton;

View File

@@ -0,0 +1,29 @@
@import '../../../../../../../admin/styles';
.button-rich-text-button {
.btn {
margin-right: base(1);
}
&__modal {
@include blur-bg;
display: flex;
align-items: center;
height: 100%;
}
&__header {
width: 100%;
display: flex;
justify-content: space-between;
h4 {
margin: 0;
}
svg {
width: base(1.5);
height: base(1.5);
}
}
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import './index.scss';
const baseClass = 'rich-text-button';
const ButtonElement = ({ attributes, children, element }) => {
const { label, style = 'primary' } = element;
return (
<span
{...attributes}
className={[
baseClass,
`${baseClass}--${style}`,
].join(' ')}
>
{label}
{children}
</span>
);
};
ButtonElement.defaultProps = {
attributes: {},
children: null,
};
ButtonElement.propTypes = {
attributes: PropTypes.shape({}),
children: PropTypes.node,
element: PropTypes.shape({
style: PropTypes.oneOf(['primary', 'secondary']),
label: PropTypes.string,
}).isRequired,
};
export default ButtonElement;

View File

@@ -0,0 +1,16 @@
@import '../../../../../../../admin/styles.scss';
.rich-text-button {
padding: base(.5) base(1.5);
border-radius: $style-radius-s;
display: inline-flex;
&--primary {
background-color: $color-dark-gray;
color: white;
}
&--secondary {
background-color: $color-light-gray;
}
}

View File

@@ -0,0 +1,10 @@
const button = require('./Button');
const element = require('./Element');
const plugin = require('./plugin');
module.exports = {
name: 'button',
button,
element,
plugin,
};

View File

@@ -0,0 +1,10 @@
const withButton = (incomingEditor) => {
const editor = incomingEditor;
const { isVoid } = editor;
editor.isVoid = (element) => (element.type === 'button' ? true : isVoid(element));
return editor;
};
export default withButton;

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { LeafButton } from '../../../../../../../admin/rich-text';
const Button = () => (
<LeafButton format="purple-background">
Purple Background
</LeafButton>
);
export default Button;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
const PurpleBackground = ({ attributes, children }) => (
<span
{...attributes}
style={{ backgroundColor: 'purple' }}
>
{children}
</span>
);
PurpleBackground.defaultProps = {
attributes: {},
children: null,
};
PurpleBackground.propTypes = {
attributes: PropTypes.shape({}),
children: PropTypes.node,
};
export default PurpleBackground;

View File

@@ -0,0 +1,8 @@
const button = require('./Button');
const leaf = require('./Leaf');
module.exports = {
name: 'purple-background',
button,
leaf,
};

View File

@@ -21,8 +21,13 @@ module.exports = {
},
auth: {
tokenExpiration: 7200,
emailVerification: false,
useAPIKey: true,
secureCookie: process.env.NODE_ENV === 'production',
cookies: {
secure: process.env.NODE_ENV === 'production',
sameSite: 'Lax',
domain: undefined,
},
},
fields: [
{

View File

@@ -151,11 +151,18 @@ const AllFields = {
label: 'Array Text 1',
type: 'text',
required: true,
}, {
admin: {
width: '50%',
},
},
{
name: 'arrayText2',
label: 'Array Text 2',
type: 'text',
required: true,
admin: {
width: '50%',
},
access: {
read: ({ req: { user } }) => Boolean(user),
update: ({ req: { user } }) => checkRole(['admin'], user),
@@ -171,6 +178,11 @@ const AllFields = {
readOnly: true,
},
},
{
name: 'checkbox',
label: 'Checkbox',
type: 'checkbox',
},
],
},
{

View File

@@ -10,6 +10,9 @@ const Code = {
type: 'code',
label: 'Code',
required: true,
admin: {
language: 'js',
},
},
],
};

View File

@@ -3,16 +3,14 @@ import PropTypes from 'prop-types';
import './index.scss';
const Filter = ({ onChange, value }) => {
return (
<input
className="custom-description-filter"
type="text"
onChange={e => onChange(e.target.value)}
value={value}
/>
);
};
const Filter = ({ onChange, value }) => (
<input
className="custom-description-filter"
type="text"
onChange={(e) => onChange(e.target.value)}
value={value}
/>
);
Filter.defaultProps = {
value: '',

View File

@@ -4,15 +4,13 @@ import DefaultList from '../../../../../../src/client/components/views/collectio
import './index.scss';
const CustomListView = (props) => {
return (
<div className="custom-list">
<p>This is a custom Pages list view</p>
<p>Sup</p>
<DefaultList {...props} />
</div>
);
};
const CustomListView = (props) => (
<div className="custom-list">
<p>This is a custom Pages list view</p>
<p>Sup</p>
<DefaultList {...props} />
</div>
);
CustomListView.propTypes = {
collection: PropTypes.shape({

View File

@@ -1,4 +1,11 @@
const path = require('path');
const DescriptionField = require('./components/fields/Description/Field');
const DescriptionCell = require('./components/fields/Description/Cell');
const DescriptionFilter = require('./components/fields/Description/Filter');
const NestedArrayField = require('./components/fields/NestedArrayCustomField/Field');
const GroupField = require('./components/fields/Group/Field');
const NestedGroupField = require('./components/fields/NestedGroupCustomField/Field');
const NestedText1Field = require('./components/fields/NestedText1/Field');
const ListView = require('./components/views/List');
module.exports = {
slug: 'custom-components',
@@ -25,9 +32,9 @@ module.exports = {
localized: true,
admin: {
components: {
field: path.resolve(__dirname, 'components/fields/Description/Field/index.js'),
cell: path.resolve(__dirname, 'components/fields/Description/Cell/index.js'),
filter: path.resolve(__dirname, 'components/fields/Description/Filter/index.js'),
field: DescriptionField,
cell: DescriptionCell,
filter: DescriptionFilter,
},
},
},
@@ -42,7 +49,7 @@ module.exports = {
label: 'Nested Array Custom Field',
admin: {
components: {
field: path.resolve(__dirname, 'components/fields/NestedArrayCustomField/Field/index.js'),
field: NestedArrayField,
},
},
},
@@ -54,7 +61,7 @@ module.exports = {
type: 'group',
admin: {
components: {
field: path.resolve(__dirname, 'components/fields/Group/Field/index.js'),
field: GroupField,
},
},
fields: [
@@ -64,7 +71,7 @@ module.exports = {
label: 'Nested Group Custom Field',
admin: {
components: {
field: path.resolve(__dirname, 'components/fields/NestedGroupCustomField/Field/index.js'),
field: NestedGroupField,
},
},
},
@@ -79,7 +86,7 @@ module.exports = {
type: 'text',
admin: {
components: {
field: path.resolve(__dirname, 'components/fields/NestedText1/Field/index.js'),
field: NestedText1Field,
},
},
}, {
@@ -95,7 +102,7 @@ module.exports = {
useAsTitle: 'title',
components: {
views: {
List: path.resolve(__dirname, 'components/views/List/index.js'),
List: ListView,
},
},
},

View File

@@ -1,4 +1,3 @@
const path = require('path');
const checkRole = require('../access/checkRole');
const access = ({ req: { user } }) => {
@@ -15,6 +14,8 @@ const access = ({ req: { user } }) => {
},
};
}
return false;
};
module.exports = {
@@ -25,7 +26,7 @@ module.exports = {
},
upload: {
staticURL: '/files',
staticDir: path.resolve(__dirname, '../files'),
staticDir: './files',
},
access: {
create: () => true,

View File

@@ -15,14 +15,6 @@ module.exports = {
delete: () => true,
},
hooks: {
beforeCreate: [
(operation) => {
if (operation.req.headers.hook === 'beforeCreate') {
operation.req.body.description += '-beforeCreateSuffix';
}
return operation.data;
},
],
beforeRead: [
(operation) => {
if (operation.req.headers.hook === 'beforeRead') {
@@ -30,10 +22,10 @@ module.exports = {
}
},
],
beforeUpdate: [
beforeChange: [
(operation) => {
if (operation.req.headers.hook === 'beforeUpdate') {
operation.req.body.description += '-beforeUpdateSuffix';
if (operation.req.headers.hook === 'beforeChange') {
operation.req.body.description += '-beforeChangeSuffix';
}
return operation.data;
},
@@ -46,14 +38,6 @@ module.exports = {
}
},
],
afterCreate: [
(operation) => {
if (operation.req.headers.hook === 'afterCreate') {
operation.doc.afterCreateHook = true;
}
return operation.doc;
},
],
afterRead: [
(operation) => {
const { doc } = operation;
@@ -62,10 +46,10 @@ module.exports = {
return doc;
},
],
afterUpdate: [
afterChange: [
(operation) => {
if (operation.req.headers.hook === 'afterUpdate') {
operation.doc.afterUpdateHook = true;
if (operation.req.headers.hook === 'afterChange') {
operation.doc.afterChangeHook = true;
}
return operation.doc;
},

View File

@@ -0,0 +1,36 @@
const LocalOperations = {
slug: 'local-operations',
labels: {
singular: 'Local Operation',
plural: 'Local Operations',
},
hooks: {
afterRead: [
async ({ req, doc }) => {
const formattedData = { ...doc };
const localizedPosts = await req.payload.find({
collection: 'localized-posts',
});
const blocksGlobal = await req.payload.findGlobal({
global: 'blocks-global',
});
formattedData.localizedPosts = localizedPosts;
formattedData.blocksGlobal = blocksGlobal;
return formattedData;
},
],
},
fields: [
{
name: 'title',
type: 'text',
label: 'title',
required: true,
},
],
};
module.exports = LocalOperations;

View File

@@ -16,7 +16,7 @@ module.exports = {
read: () => true,
},
preview: (doc, token) => {
if (doc.title) {
if (doc && doc.title) {
return `http://localhost:3000/posts/${doc.title.value}?preview=true&token=${token}`;
}
@@ -32,6 +32,12 @@ module.exports = {
unique: true,
localized: true,
},
{
name: 'summary',
label: 'Summary',
type: 'text',
index: true,
},
{
name: 'description',
label: 'Description',

View File

@@ -12,7 +12,7 @@ module.exports = {
},
upload: {
staticURL: '/media',
staticDir: path.resolve(__dirname, '../media'),
staticDir: './media',
adminThumbnail: 'mobile',
imageSizes: [
{

View File

@@ -32,7 +32,13 @@ module.exports = {
},
auth: {
tokenExpiration: 300,
secureCookie: process.env.NODE_ENV === 'production',
emailVerification: true,
generateVerificationUrl: (req, token) => `http://localhost:3000/api/verify?token=${token}`,
cookies: {
secure: process.env.NODE_ENV === 'production',
sameSite: 'Lax',
domain: undefined,
},
},
fields: [
{

View File

@@ -1,3 +1,6 @@
const Button = require('../client/components/richText/elements/Button');
const PurpleBackground = require('../client/components/richText/leaves/PurpleBackground');
const RichText = {
slug: 'rich-text',
labels: {
@@ -10,8 +13,28 @@ const RichText = {
type: 'richText',
label: 'Rich Text',
required: true,
disabledElements: [],
disabledMarks: [],
admin: {
elements: [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
// 'blockquote',
Button,
'ul',
'ol',
'link',
],
leaves: [
'bold',
'italic',
PurpleBackground,
// 'underline',
// 'strikethrough',
],
},
},
],
};

View File

@@ -0,0 +1,43 @@
const Select = {
slug: 'select',
labels: {
singular: 'Select',
plural: 'Selects',
},
fields: [
{
name: 'Select',
type: 'select',
options: [{
value: 'one',
label: 'One',
}, {
value: 'two',
label: 'Two',
}, {
value: 'three',
label: 'Three',
}],
label: 'Select From',
required: true,
},
{
name: 'Radio',
type: 'radio',
options: [{
value: 'one',
label: 'One',
}, {
value: 'two',
label: 'Two',
}, {
value: 'three',
label: 'Three',
}],
label: 'Choose From',
required: true,
},
],
};
module.exports = Select;

15
demo/custom-index.html Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="application-name" content="My Payload Application" />
</head>
<body>
<div id="app"></div>
<div id="portal"></div>
</body>
</html>

View File

@@ -1,4 +1,3 @@
const path = require('path');
const Admin = require('./collections/Admin');
const AllFields = require('./collections/AllFields');
const Code = require('./collections/Code');
@@ -10,6 +9,7 @@ const HiddenFields = require('./collections/HiddenFields');
const Hooks = require('./collections/Hooks');
const Localized = require('./collections/Localized');
const LocalizedArray = require('./collections/LocalizedArray');
const LocalOperations = require('./collections/LocalOperations');
const Media = require('./collections/Media');
const NestedArrays = require('./collections/NestedArrays');
const Preview = require('./collections/Preview');
@@ -17,6 +17,7 @@ const PublicUsers = require('./collections/PublicUsers');
const RelationshipA = require('./collections/RelationshipA');
const RelationshipB = require('./collections/RelationshipB');
const RichText = require('./collections/RichText');
const Select = require('./collections/Select');
const StrictPolicies = require('./collections/StrictPolicies');
const Validations = require('./collections/Validations');
@@ -27,13 +28,23 @@ const GlobalWithStrictAccess = require('./globals/GlobalWithStrictAccess');
module.exports = {
admin: {
user: 'admins',
// indexHTML: 'custom-index.html',
meta: {
titleSuffix: '- Payload Demo',
// ogImage: '/static/find-image-here.jpg',
// favicon: '/img/whatever.png',
},
disable: false,
components: {
layout: {
// Sidebar: path.resolve(__dirname, 'client/components/layout/Sidebar/index.js'),
},
// nav: () => (
// <div>Hello</div>
// ),
},
},
email: {
fromName: 'Payload',
fromAddress: 'hello@payloadcms.com',
},
collections: [
Admin,
AllFields,
@@ -46,6 +57,7 @@ module.exports = {
Hooks,
Localized,
LocalizedArray,
LocalOperations,
Media,
NestedArrays,
Preview,
@@ -53,6 +65,7 @@ module.exports = {
RelationshipA,
RelationshipB,
RichText,
Select,
StrictPolicies,
Validations,
],
@@ -63,7 +76,16 @@ module.exports = {
],
cookiePrefix: 'payload',
serverURL: 'http://localhost:3000',
cors: ['http://localhost', 'http://localhost:8080', 'http://localhost:8081'],
cors: [
'http://localhost',
'http://localhost:3000',
'http://localhost:8080',
'http://localhost:8081',
],
csrf: [
'http://localhost:3000',
'https://other-app-here.com',
],
routes: {
api: '/api',
admin: '/admin',
@@ -73,7 +95,7 @@ module.exports = {
defaultDepth: 2,
compression: {},
paths: {
scss: path.resolve(__dirname, 'client/scss/overrides.scss'),
scss: 'client/scss/overrides.scss',
},
graphQL: {
mutations: {},

View File

@@ -1,27 +1,29 @@
/* eslint-disable no-console */
const express = require('express');
const path = require('path');
const Payload = require('../src');
const payload = require('../src');
const logger = require('../src/utilities/logger')();
const expressApp = express();
expressApp.use('/static', express.static(path.resolve(__dirname, 'client/static')));
const payload = new Payload({
payload.init({
email: {
provider: 'mock',
transport: 'mock',
},
secret: 'SECRET_KEY',
mongoURL: 'mongodb://localhost/payload',
express: expressApp,
onInit: () => {
console.log('Payload is initialized');
logger.info('Payload is initialized');
// console.log('Payload is initialized');
},
});
const externalRouter = express.Router();
externalRouter.use(payload.authenticate());
externalRouter.use(payload.authenticate);
externalRouter.get('/', (req, res) => {
if (req.user) {
@@ -33,17 +35,16 @@ externalRouter.get('/', (req, res) => {
expressApp.use('/external-route', externalRouter);
exports.payload = payload;
exports.start = (cb) => {
const server = expressApp.listen(3000, async () => {
console.log(`listening on ${3000}...`);
if (cb) cb();
logger.info(`listening on ${3000}...`);
const creds = await payload.getMockEmailCredentials();
console.log(`Mock email account username: ${creds.user}`);
console.log(`Mock email account password: ${creds.pass}`);
console.log(`Log in to mock email provider at ${creds.web}`);
logger.info(`Mock email account username: ${creds.user}`);
logger.info(`Mock email account password: ${creds.pass}`);
logger.info(`Log in to mock email provider at ${creds.web}`);
if (cb) cb();
});
return server;

View File

@@ -3,4 +3,9 @@ module.exports = {
testEnvironment: 'node',
globalSetup: '<rootDir>/src/tests/globalSetup.js',
globalTeardown: '<rootDir>/src/tests/globalTeardown.js',
testTimeout: 15000,
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/src/mocks/fileMock.js',
'\\.(css|scss)$': '<rootDir>/src/mocks/emptyModule.js',
},
};

3
logger.js Normal file
View File

@@ -0,0 +1,3 @@
const logger = require('./src/utilities/logger');
module.exports = logger;

View File

@@ -1,10 +1,11 @@
{
"name": "@payloadcms/payload",
"version": "0.0.24",
"version": "0.0.82",
"description": "CMS and Application Framework",
"license": "ISC",
"author": "Payload CMS LLC",
"main": "index.js",
"typings": "payload.d.ts",
"bin": {
"payload": "./src/bin/index.js"
},
@@ -18,23 +19,23 @@
"test:unit": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.js NODE_ENV=test jest"
},
"dependencies": {
"@babel/core": "^7.8.3",
"@babel/core": "^7.11.6",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.8.3",
"@babel/preset-react": "^7.8.3",
"@babel/runtime": "^7.8.4",
"@babel/register": "^7.11.5",
"@date-io/date-fns": "^1.3.13",
"@faceless-ui/collapsibles": "^0.1.0",
"@faceless-ui/modal": "^1.0.4",
"@faceless-ui/scroll-info": "^1.1.1",
"@faceless-ui/window-info": "^1.2.2",
"@udecode/slate-plugins": "^0.60.0",
"@udecode/slate-plugins": "^0.64.3",
"async-some": "^1.0.2",
"autoprefixer": "^9.7.4",
"babel-core": "^7.0.0-bridge.0",
"babel-loader": "^8.0.6",
"babel-jest": "^26.3.0",
"babel-loader": "^8.1.0",
"babel-plugin-add-module-exports": "^1.0.4",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
"connect-history-api-fallback": "^1.6.0",
@@ -48,6 +49,7 @@
"express-fileupload": "^1.1.6",
"express-graphql": "^0.9.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"falsey": "^1.0.0",
"file-loader": "^1.1.11",
"flatley": "^5.2.0",
"graphql": "^15.0.0",
@@ -56,12 +58,15 @@
"graphql-type-json": "^0.3.1",
"html-webpack-plugin": "^3.2.0",
"http-status": "^1.4.2",
"ignore-styles": "^5.0.1",
"image-size": "^0.7.5",
"is-hotkey": "^0.1.6",
"is-url": "^1.2.4",
"isomorphic-fetch": "^2.2.1",
"jest": "^25.3.0",
"jsonwebtoken": "^8.5.1",
"method-override": "^3.0.0",
"micro-memoize": "^4.0.9",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"mongodb-memory-server": "^6.5.2",
@@ -79,9 +84,12 @@
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"passport-local-mongoose": "^6.0.1",
"pino": "^6.4.1",
"pino-pretty": "^4.1.0",
"postcss-flexbugs-fixes": "^3.3.1",
"postcss-loader": "^2.1.6",
"postcss-preset-env": "6.0.6",
"prismjs": "^1.21.0",
"prop-types": "^15.7.2",
"qs": "^6.9.1",
"qs-middleware": "^1.0.3",
@@ -91,24 +99,21 @@
"react-datepicker": "^2.13.0",
"react-document-meta": "^3.0.0-beta.2",
"react-dom": "^16.13.1",
"react-hook-form": "^5.7.2",
"react-redux": "^7.2.0",
"react-helmet": "^6.1.0",
"react-router-dom": "^5.1.2",
"react-router-navigation-prompt": "^1.8.11",
"react-select": "^3.0.8",
"react-simple-code-editor": "^0.11.0",
"sanitize-filename": "^1.6.3",
"sass-loader": "7.1.0",
"sharp": "^0.25.2",
"slate": "^0.58.3",
"slate": "^0.58.4",
"slate-history": "^0.58.3",
"slate-hyperscript": "^0.58.3",
"slate-react": "^0.58.3",
"style-loader": "^0.21.0",
"styled-components": "^5.1.1",
"uglifyjs-webpack-plugin": "^2.2.0",
"url-loader": "^1.0.1",
"uuid": "^8.1.0",
"val-loader": "^2.1.0",
"webpack": "^4.43.0",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-dev-middleware": "^3.7.2",
@@ -127,6 +132,7 @@
"eslint-plugin-react": "^7.18.0",
"eslint-plugin-react-hooks": "^2.3.0",
"faker": "^4.1.0",
"form-data": "^3.0.0",
"graphql-request": "^2.0.0",
"nodemon": "^1.19.4"
}

170
payload.d.ts vendored Normal file
View File

@@ -0,0 +1,170 @@
declare module "@payloadcms/payload" {
export interface PayloadEmailOptions {
transport: 'mock'; // TODO: import nodemailer Mail type
fromName?: string;
fromAddress?: string;
}
export interface PayloadInitOptions {
express: any,
mongoURL: string,
secret: string,
email: PayloadEmailOptions,
onInit?: () => void,
}
export interface Document {
id: string;
}
export interface CreateOptions {
collection: string;
data: any;
}
export interface FindOptions {
collection: string;
}
export interface FindResponse {
docs: Document[];
totalDocs: number;
limit: number;
totalPages: number;
page: number;
pagingCounter: number;
hasPrevPage: boolean;
hasNextPage: boolean;
prevPage: number | null;
nextPage: number | null;
}
export interface FindGlobalOptions {
global: string;
}
export interface UpdateGlobalOptions {
global: string;
data: any;
}
export interface FindByIDOptions {
collection: string;
id: string;
}
export interface UpdateOptions {
collection: string;
id: string;
data: any;
}
export interface DeleteOptions {
collection: string;
id: string;
}
export interface ForgotPasswordOptions {
collection: string;
generateEmailHTML?: (token: string) => string;
expiration: Date;
data: any;
}
export interface SendEmailOptions {
from: string;
to: string;
subject: string;
html: string;
}
export interface MockEmailCredentials {
user: string;
pass: string;
web: string;
}
class PayloadInstance {
init(options: PayloadInitOptions): void;
sendEmail(message: SendEmailOptions): void;
getMockEmailCredentials(): MockEmailCredentials;
create(options: CreateOptions): Promise<Document>;
find(options: FindOptions): Promise<FindResponse>;
findGlobal(options: FindGlobalOptions): Promise<Document>;
updateGlobal(options: UpdateGlobalOptions): Promise<Document>;
findByID(options: FindByIDOptions): Promise<Document>;
update(options: UpdateOptions): Promise<Document>;
delete(options: DeleteOptions): Promise<Document>;
login(options: any): Promise<any>; // TODO: find example of this to type
forgotPassword(options: ForgotPasswordOptions): Promise<any>;
resetPassword(options: any): Promise<any>;
}
var payload: PayloadInstance;
export default payload;
}
declare module "@payloadcms/payload/types" {
export interface PayloadField {
name: string;
label: string;
type: 'number' | 'text' | 'email' | 'textarea' | 'richText' | 'code' | 'radio' | 'checkbox' | 'date' | 'upload' | 'relationship' | 'row' | 'array' | 'group' | 'select' | 'blocks';
localized?: boolean;
fields?: PayloadField[];
}
export interface PayloadCollection {
slug: string;
labels: {
singular: string;
plural: string;
},
access?: {
create?: (args?: any) => boolean;
read?: (args?: any) => boolean;
update?: (args?: any) => boolean;
delete?: (args?: any) => boolean;
admin?: (args?: any) => boolean;
},
fields: PayloadField[];
}
export interface PayloadGlobal {
slug: string;
label: string;
access?: {
create?: (args?: any) => boolean;
read?: (args?: any) => boolean;
update?: (args?: any) => boolean;
delete?: (args?: any) => boolean;
admin?: (args?: any) => boolean;
};
fields: PayloadField[];
}
export interface PayloadConfig {
admin?: {
user?: string;
meta?: {
titleSuffix?: string;
},
disable?: boolean;
};
collections?: PayloadCollection[];
globals?: PayloadGlobal[];
routes?: {
api?: string;
admin?: string;
graphQL?: string;
graphQLPlayground?: string;
};
defaultDepth?: number,
localization?: {
locales: string[]
};
defaultLocale?: string;
fallback?: boolean;
productionGraphQLPlayground?: boolean;
hooks?: {
afterError?: () => void;
};
webpack?: (config: any) => any;
}
}

View File

@@ -6,9 +6,9 @@ const { email, password } = require('../tests/credentials');
* @jest-environment node
*/
const config = require('../../demo/payload.config');
const getConfig = require('../utilities/getConfig');
const url = config.serverURL;
const { serverURL: url } = getConfig();
let token = null;
@@ -97,7 +97,7 @@ describe('Users REST API', () => {
});
it('should allow a user to be created', async () => {
const response = await fetch(`${url}/api/admins/register`, {
const response = await fetch(`${url}/api/admins`, {
body: JSON.stringify({
email: `${faker.name.firstName()}@test.com`,
password,

View File

@@ -10,14 +10,4 @@ module.exports = [
disabled: true,
},
},
{
name: 'resetPasswordToken',
type: 'text',
hidden: true,
},
{
name: 'resetPasswordExpiration',
type: 'date',
hidden: true,
},
];

View File

@@ -10,6 +10,7 @@ const defaultUser = {
auth: {
tokenExpiration: 7200,
},
fields: [],
timestamps: true,
};

View File

@@ -1,18 +1,23 @@
const parseCookies = require('../utilities/parseCookies');
const getExtractJWT = config => (req) => {
const jwtFromHeader = req.get('Authorization');
const getExtractJWT = (config) => (req) => {
if (req && req.get) {
const jwtFromHeader = req.get('Authorization');
const origin = req.get('Origin');
if (jwtFromHeader && jwtFromHeader.indexOf('JWT ') === 0) {
return jwtFromHeader.replace('JWT ', '');
}
if (jwtFromHeader && jwtFromHeader.indexOf('JWT ') === 0) {
return jwtFromHeader.replace('JWT ', '');
}
const cookies = parseCookies(req);
const tokenCookieName = `${config.cookiePrefix}-token`;
const cookies = parseCookies(req);
const tokenCookieName = `${config.cookiePrefix}-token`;
if (cookies && cookies[tokenCookieName]) {
const token = cookies[tokenCookieName];
return token;
if (cookies && cookies[tokenCookieName]) {
if (!origin || (config.csrf && config.csrf.indexOf(origin) > -1)) {
const token = cookies[tokenCookieName];
return token;
}
}
}
return null;

View File

@@ -2,8 +2,10 @@ function forgotPassword(collection) {
async function resolver(_, args, context) {
const options = {
collection,
data: args,
data: { email: args.email },
req: context.req,
disableEmail: args.disableEmail,
expiration: args.expiration,
};
await this.operations.collections.auth.forgotPassword(options);

View File

@@ -3,7 +3,7 @@ const getExtractJWT = require('../../getExtractJWT');
function refresh(collection) {
async function resolver(_, __, context) {
const extractJWT = getExtractJWT(this.config);
const token = extractJWT(context);
const token = extractJWT(context.req);
const options = {
collection,

View File

@@ -1,30 +0,0 @@
/* eslint-disable no-param-reassign */
function register(collection) {
async function resolver(_, args, context) {
const options = {
collection,
data: args.data,
depth: 0,
req: context.req,
};
if (args.locale) {
context.req.locale = args.locale;
options.locale = args.locale;
}
if (args.fallbackLocale) {
context.req.fallbackLocale = args.fallbackLocale;
options.fallbackLocale = args.fallbackLocale;
}
const token = await this.operations.collections.auth.register(options);
return token;
}
const registerResolver = resolver.bind(this);
return registerResolver;
}
module.exports = register;

View File

@@ -8,6 +8,7 @@ function resetPassword(collection) {
collection,
data: args,
req: context.req,
res: context.res,
api: 'GraphQL',
};

View File

@@ -1,25 +0,0 @@
/* eslint-disable no-param-reassign */
function update(collection) {
async function resolver(_, args, context) {
if (args.locale) context.req.locale = args.locale;
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale;
const options = {
collection,
data: args.data,
id: args.id,
depth: 0,
req: context.req,
};
const user = await this.operations.collections.auth.update(options);
return user;
}
const updateResolver = resolver.bind(this);
return updateResolver;
}
module.exports = update;

View File

@@ -29,43 +29,51 @@ async function forgotPassword(args) {
Model,
},
data,
disableEmail,
expiration,
} = options;
let token = await crypto.randomBytes(20);
let token = crypto.randomBytes(20);
token = token.toString('hex');
const user = await Model.findOne({ email: data.email });
const user = await Model.findOne({ email: data.email.toLowerCase() });
if (!user) return;
if (!user) return null;
user.resetPasswordToken = token;
user.resetPasswordExpiration = Date.now() + 3600000; // 1 hour
user.resetPasswordExpiration = expiration || Date.now() + 3600000; // 1 hour
await user.save();
const html = `You are receiving this because you (or someone else) have requested the reset of the password for your account.
Please click on the following link, or paste this into your browser to complete the process:
<a href="${config.serverURL}${config.routes.admin}/reset/${token}">
${config.serverURL}${config.routes.admin}/reset/${token}
</a>
If you did not request this, please ignore this email and your password will remain unchanged.`;
if (!disableEmail) {
let html = `You are receiving this because you (or someone else) have requested the reset of the password for your account.
Please click on the following link, or paste this into your browser to complete the process:
<a href="${config.serverURL}${config.routes.admin}/reset/${token}">
${config.serverURL}${config.routes.admin}/reset/${token}
</a>
If you did not request this, please ignore this email and your password will remain unchanged.`;
email({
from: `"${config.email.fromName}" <${config.email.fromAddress}>`,
to: data.email,
subject: 'Password Reset',
html,
});
if (args.generateEmailHTML) html = args.generateEmailHTML(token);
email({
from: `"${config.email.fromName}" <${config.email.fromAddress}>`,
to: data.email,
subject: 'Password Reset',
html,
});
}
// /////////////////////////////////////
// 3. Execute after forgot password hook
// /////////////////////////////////////
const { afterForgotPassword } = args.req.collection.config.hooks;
const { afterForgotPassword } = args.collection.config.hooks;
if (typeof afterForgotPassword === 'function') {
await afterForgotPassword(options);
}
return token;
}
module.exports = forgotPassword;

View File

@@ -0,0 +1,22 @@
async function forgotPassword(options) {
const {
collection: collectionSlug,
data,
expiration,
disableEmail,
generateEmailHTML,
} = options;
const collection = this.collections[collectionSlug];
return this.operations.collections.auth.forgotPassword({
data,
collection,
overrideAccess: true,
disableEmail,
expiration,
generateEmailHTML,
});
}
module.exports = forgotPassword;

View File

@@ -0,0 +1,9 @@
const login = require('./login');
const forgotPassword = require('./forgotPassword');
const resetPassword = require('./resetPassword');
module.exports = {
login,
forgotPassword,
resetPassword,
};

View File

@@ -0,0 +1,33 @@
async function login(args) {
const {
collection: collectionSlug,
req = {},
res,
depth,
locale,
fallbackLocale,
data,
} = args;
const collection = this.collections[collectionSlug];
const options = {
depth,
collection,
overrideAccess: true,
data,
req: {
...req,
payloadAPI: 'local',
payload: this,
},
res,
};
if (locale) options.req.locale = locale;
if (fallbackLocale) options.req.fallbackLocale = fallbackLocale;
return this.operations.collections.auth.login(options);
}
module.exports = login;

View File

@@ -0,0 +1,24 @@
async function resetPassword(options) {
const {
collection: collectionSlug,
data,
req,
res,
} = options;
const collection = this.collections[collectionSlug];
return this.operations.collections.auth.resetPassword({
data,
collection,
overrideAccess: true,
req: {
...req,
payloadAPI: 'local',
payload: this,
},
res,
});
}
module.exports = resetPassword;

View File

@@ -1,5 +1,6 @@
const jwt = require('jsonwebtoken');
const { AuthenticationError } = require('../../errors');
const getCookieExpiration = require('../../utilities/getCookieExpiration');
async function login(args) {
const { config, operations } = this;
@@ -25,13 +26,15 @@ async function login(args) {
req,
} = options;
const { email, password } = data;
const { email: unsanitizedEmail, password } = data;
const email = unsanitizedEmail ? unsanitizedEmail.toLowerCase() : null;
const userDoc = await Model.findByUsername(email);
if (!userDoc) throw new AuthenticationError();
if (!userDoc || (args.collection.config.auth.emailVerification && !userDoc._verified)) {
throw new AuthenticationError();
}
const authResult = await userDoc.authenticate(password);
if (!authResult.user) {
@@ -55,20 +58,29 @@ async function login(args) {
const user = userQuery.docs[0];
const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => {
if (field.saveToJWT) {
return {
...signedFields,
[field.name]: user[field.name],
};
const result = {
...signedFields,
};
if (!field.name && field.fields) {
field.fields.forEach((subField) => {
if (subField.saveToJWT) {
result[subField.name] = user[subField.name];
}
});
}
return signedFields;
if (field.saveToJWT) {
result[field.name] = user[field.name];
}
return result;
}, {
email,
id: user.id,
collection: collectionConfig.slug,
});
fieldsToSign.collection = collectionConfig.slug;
const token = jwt.sign(
fieldsToSign,
config.secret,
@@ -81,17 +93,13 @@ async function login(args) {
const cookieOptions = {
path: '/',
httpOnly: true,
expires: getCookieExpiration(collectionConfig.auth.tokenExpiration),
secure: collectionConfig.auth.cookies.secure,
sameSite: collectionConfig.auth.cookies.sameSite,
};
if (collectionConfig.auth.secureCookie) {
cookieOptions.secure = true;
}
if (args.req.headers.origin && args.req.headers.origin.indexOf('localhost') === -1) {
let domain = args.req.headers.origin.replace('https://', '');
domain = args.req.headers.origin.replace('http://', '');
cookieOptions.domain = domain;
}
if (collectionConfig.auth.cookies.domain) cookieOptions.domain = collectionConfig.auth.cookies.domain;
args.res.cookie(`${config.cookiePrefix}-token`, token, cookieOptions);
}

View File

@@ -2,31 +2,14 @@ async function logout(args) {
const { config } = this;
const {
collection: {
config: collectionConfig,
},
res,
req,
} = args;
const cookieOptions = {
expires: new Date(0),
httpOnly: true,
path: '/',
overwrite: true,
};
if (collectionConfig.auth && collectionConfig.auth.secureCookie) {
cookieOptions.secure = true;
}
if (req.headers.origin && req.headers.origin.indexOf('localhost') === -1) {
let domain = req.headers.origin.replace('https://', '');
domain = req.headers.origin.replace('http://', '');
cookieOptions.domain = domain;
}
res.cookie(`${config.cookiePrefix}-token`, '', cookieOptions);
res.clearCookie(`${config.cookiePrefix}-token`, cookieOptions);
return 'Logged out successfully.';
}

View File

@@ -1,10 +1,10 @@
const jwt = require('jsonwebtoken');
const { Forbidden } = require('../../errors');
const getCookieExpiration = require('../../utilities/getCookieExpiration');
async function refresh(args) {
// Await validation here
const { secret, cookiePrefix } = this.config;
let options = { ...args };
// /////////////////////////////////////
@@ -21,33 +21,35 @@ async function refresh(args) {
// 2. Perform refresh
// /////////////////////////////////////
const {
collection: {
config: collectionConfig,
},
} = options;
const opts = {};
opts.expiresIn = options.collection.config.auth.tokenExpiration;
if (typeof options.token !== 'string') throw new Forbidden();
const payload = jwt.verify(options.token, secret, {});
const payload = jwt.verify(options.token, this.config.secret, {});
delete payload.iat;
delete payload.exp;
const refreshedToken = jwt.sign(payload, secret, opts);
const refreshedToken = jwt.sign(payload, this.config.secret, opts);
if (args.res) {
const cookieOptions = {
path: '/',
httpOnly: true,
expires: getCookieExpiration(collectionConfig.auth.tokenExpiration),
secure: collectionConfig.auth.cookies.secure,
sameSite: collectionConfig.auth.cookies.sameSite,
};
if (options.collection.config.auth.secureCookie) {
cookieOptions.secure = true;
}
if (args.req.headers.origin && args.req.headers.origin.indexOf('localhost') === -1) {
let domain = args.req.headers.origin.replace('https://', '');
domain = args.req.headers.origin.replace('http://', '');
cookieOptions.domain = domain;
}
if (collectionConfig.auth.cookies.domain) cookieOptions.domain = collectionConfig.auth.cookies.domain;
args.res.cookie(`${cookiePrefix}-token`, refreshedToken, cookieOptions);
args.res.cookie(`${this.config.cookiePrefix}-token`, refreshedToken, cookieOptions);
}

View File

@@ -1,106 +0,0 @@
const passport = require('passport');
const executeAccess = require('../executeAccess');
async function register(args) {
const {
depth,
overrideAccess,
collection: {
Model,
config: collectionConfig,
},
req,
req: {
locale,
fallbackLocale,
},
} = args;
let { data } = args;
// /////////////////////////////////////
// 1. Retrieve and execute access
// /////////////////////////////////////
if (!overrideAccess) {
await executeAccess({ req }, collectionConfig.access.create);
}
// /////////////////////////////////////
// 2. Execute before create hook
// /////////////////////////////////////
await collectionConfig.hooks.beforeCreate.reduce(async (priorHook, hook) => {
await priorHook;
data = (await hook({
data,
req,
})) || data;
}, Promise.resolve());
// /////////////////////////////////////
// 3. Execute field-level hooks, access, and validation
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data,
hook: 'beforeCreate',
operationName: 'create',
req,
});
// /////////////////////////////////////
// 6. Perform register
// /////////////////////////////////////
const modelData = { ...data };
delete modelData.password;
const user = new Model();
if (locale && user.setLocale) {
user.setLocale(locale, fallbackLocale);
}
Object.assign(user, modelData);
let result = await Model.register(user, data.password);
await passport.authenticate('local');
result = result.toJSON({ virtuals: true });
// /////////////////////////////////////
// 7. Execute field-level hooks and access
// /////////////////////////////////////
result = await this.performFieldOperations(collectionConfig, {
data: result,
hook: 'afterRead',
operationName: 'read',
req,
depth,
});
// /////////////////////////////////////
// 8. Execute after create hook
// /////////////////////////////////////
await collectionConfig.hooks.afterCreate.reduce(async (priorHook, hook) => {
await priorHook;
result = await hook({
doc: result,
req: args.req,
}) || result;
}, Promise.resolve());
// /////////////////////////////////////
// 9. Return user
// /////////////////////////////////////
return result;
}
module.exports = register;

View File

@@ -15,7 +15,7 @@ async function registerFirstUser(args) {
// 2. Perform register first user
// /////////////////////////////////////
let result = await this.operations.collections.auth.register({
let result = await this.operations.collections.create({
...args,
overrideAccess: true,
});
@@ -35,7 +35,7 @@ async function registerFirstUser(args) {
};
return {
message: 'Registered successfully. Welcome to Payload!',
message: 'Registered and logged in successfully. Welcome!',
user: result,
};
}

View File

@@ -1,5 +1,6 @@
const jwt = require('jsonwebtoken');
const { APIError } = require('../../errors');
const getCookieExpiration = require('../../utilities/getCookieExpiration');
async function resetPassword(args) {
const { config } = this;
@@ -33,8 +34,6 @@ async function resetPassword(args) {
data,
} = options;
const { email } = data;
const user = await Model.findOne({
resetPasswordToken: data.token,
resetPasswordExpiration: { $gt: Date.now() },
@@ -42,7 +41,6 @@ async function resetPassword(args) {
if (!user) throw new APIError('Token is either invalid or has expired.');
await user.setPassword(data.password);
user.resetPasswordExpiration = Date.now();
@@ -60,7 +58,9 @@ async function resetPassword(args) {
}
return signedFields;
}, {
email,
email: user.email,
id: user.id,
collection: collectionConfig.slug,
});
const token = jwt.sign(
@@ -71,6 +71,21 @@ async function resetPassword(args) {
},
);
if (args.res) {
const cookieOptions = {
path: '/',
httpOnly: true,
expires: getCookieExpiration(collectionConfig.auth.tokenExpiration),
secure: collectionConfig.auth.cookies.secure,
sameSite: collectionConfig.auth.cookies.sameSite,
};
if (collectionConfig.auth.cookies.domain) cookieOptions.domain = collectionConfig.auth.cookies.domain;
args.res.cookie(`${config.cookiePrefix}-token`, token, cookieOptions);
}
// /////////////////////////////////////
// 3. Execute after reset password hook
// /////////////////////////////////////

View File

@@ -1,142 +0,0 @@
const deepmerge = require('deepmerge');
const overwriteMerge = require('../../utilities/overwriteMerge');
const { NotFound, Forbidden } = require('../../errors');
const executeAccess = require('../executeAccess');
async function update(args) {
const { config } = this;
const {
depth,
collection: {
Model,
config: collectionConfig,
},
id,
req,
req: {
locale,
fallbackLocale,
},
} = args;
// /////////////////////////////////////
// 1. Execute access
// /////////////////////////////////////
const accessResults = await executeAccess({ req }, collectionConfig.access.update);
const hasWhereAccess = typeof accessResults === 'object';
// /////////////////////////////////////
// 2. Retrieve document
// /////////////////////////////////////
let query = { _id: id };
if (hasWhereAccess) {
query = {
...query,
...accessResults,
};
}
let user = await Model.findOne(query);
if (!user && !hasWhereAccess) throw new NotFound();
if (!user && hasWhereAccess) throw new Forbidden();
if (locale && user.setLocale) {
user.setLocale(locale, fallbackLocale);
}
const originalDoc = user.toJSON({ virtuals: true });
let { data } = args;
// /////////////////////////////////////
// 2. Execute before update hook
// /////////////////////////////////////
await collectionConfig.hooks.beforeUpdate.reduce(async (priorHook, hook) => {
await priorHook;
data = (await hook({
data,
req,
originalDoc,
})) || data;
}, Promise.resolve());
// /////////////////////////////////////
// 3. Merge updates into existing data
// /////////////////////////////////////
data = deepmerge(originalDoc, data, { arrayMerge: overwriteMerge });
// /////////////////////////////////////
// 4. Execute field-level hooks, access, and validation
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data,
req,
hook: 'beforeUpdate',
operationName: 'update',
originalDoc,
});
// /////////////////////////////////////
// 5. Handle password update
// /////////////////////////////////////
const dataToUpdate = { ...data };
const { password } = dataToUpdate;
if (password) {
delete dataToUpdate.password;
await user.setPassword(password);
}
// /////////////////////////////////////
// 6. Perform database operation
// /////////////////////////////////////
Object.assign(user, dataToUpdate);
await user.save();
user = user.toJSON({ virtuals: true });
// /////////////////////////////////////
// 7. Execute field-level hooks and access
// /////////////////////////////////////
user = this.performFieldOperations(collectionConfig, {
data: user,
hook: 'afterRead',
operationName: 'read',
req,
depth,
});
// /////////////////////////////////////
// 8. Execute after update hook
// /////////////////////////////////////
await collectionConfig.hooks.afterUpdate.reduce(async (priorHook, hook) => {
await priorHook;
user = await hook({
doc: user,
req,
}) || user;
}, Promise.resolve());
// /////////////////////////////////////
// 9. Return user
// /////////////////////////////////////
return user;
}
module.exports = update;

View File

@@ -0,0 +1,26 @@
const httpStatus = require('http-status');
const { APIError } = require('../../errors');
async function verifyEmail(args) {
if (!Object.prototype.hasOwnProperty.call(args, 'token')) {
throw new APIError('Missing required data.', httpStatus.BAD_REQUEST);
}
// /////////////////////////////////////
// 2. Perform password reset
// /////////////////////////////////////
// TODO: How do we know which collection this is?
const user = await args.collection.Model.findOne({
_verificationToken: args.token,
});
if (!user) throw new APIError('User not found.', httpStatus.BAD_REQUEST);
user._verified = true;
user._verificationToken = null;
await user.save();
}
module.exports = verifyEmail;

View File

@@ -5,7 +5,9 @@ async function forgotPasswordHandler(req, res, next) {
await this.operations.collections.auth.forgotPassword({
req,
collection: req.collection,
data: req.body,
data: { email: req.body.email },
disableEmail: req.body.disableEmail,
expiration: req.body.expiration,
});
return res.status(httpStatus.OK)

View File

@@ -1,21 +0,0 @@
const httpStatus = require('http-status');
const formatSuccessResponse = require('../../express/responses/formatSuccess');
async function register(req, res, next) {
try {
const user = await this.operations.collections.auth.register({
collection: req.collection,
req,
data: req.body,
});
return res.status(httpStatus.CREATED).json({
...formatSuccessResponse(`${req.collection.config.labels.singular} successfully created.`, 'message'),
doc: user,
});
} catch (error) {
return next(error);
}
}
module.exports = register;

View File

@@ -4,13 +4,14 @@ async function resetPassword(req, res, next) {
try {
const token = await this.operations.collections.auth.resetPassword({
req,
res,
collection: req.collection,
data: req.body,
});
return res.status(httpStatus.OK)
.json({
message: 'Password reset',
message: 'Password reset successfully.',
token,
});
} catch (error) {

View File

@@ -1,22 +0,0 @@
const httpStatus = require('http-status');
const formatSuccessResponse = require('../../express/responses/formatSuccess');
async function update(req, res, next) {
try {
const user = await this.operations.collections.auth.update({
req,
data: req.body,
collection: req.collection,
id: req.params.id,
});
return res.status(httpStatus.OK).json({
...formatSuccessResponse('Updated successfully.', 'message'),
doc: user,
});
} catch (error) {
return next(error);
}
}
module.exports = update;

View File

@@ -0,0 +1,19 @@
const httpStatus = require('http-status');
async function verifyEmail(req, res, next) {
try {
await this.operations.collections.auth.verifyEmail({
collection: req.collection,
token: req.params.token,
});
return res.status(httpStatus.OK)
.json({
message: 'Email verified successfully.',
});
} catch (error) {
return next(error);
}
}
module.exports = verifyEmail;

View File

@@ -0,0 +1,34 @@
function sendVerificationEmail(args) {
// Verify token from e-mail
const {
config,
sendEmail,
collection: {
config: collectionConfig,
},
user,
disableEmail,
req,
} = args;
if (!disableEmail) {
const url = collectionConfig.auth.generateVerificationUrl
? `${config.serverURL}${collectionConfig.auth.generateVerificationUrl(req, user._verificationToken)}`
: 'asdfasdf'; // TODO: point to payload view that verifies
const html = `Thank you for created an account.
Please click on the following link, or paste this into your browser to complete the process:
<a href="${url}">
${url}
</a>
If you did not request this, please ignore this email and your password will remain unchanged.`;
sendEmail({
from: `"${config.email.fromName}" <${config.email.fromAddress}>`,
to: user.email,
subject: 'Verify Email',
html,
});
}
}
module.exports = sendVerificationEmail;

View File

@@ -6,14 +6,29 @@ module.exports = ({ operations }, { Model, config }) => {
prefix: `${config.labels.singular} API-Key `,
};
return new PassportAPIKey(opts, true, async (req, apiKey, done) => {
return new PassportAPIKey(opts, true, async (apiKey, done, req) => {
try {
const userQuery = await operations.collections.find({
where: {
apiKey: {
equals: apiKey,
const where = {};
if (config.auth.emailVerification) {
where.and = [
{
apiKey: {
equals: apiKey,
},
},
},
{
_verified: {
equals: true,
},
},
];
} else {
where.apiKey = {
equals: apiKey,
};
}
const userQuery = await operations.collections.find({
where,
collection: {
Model,
config,

View File

@@ -18,12 +18,27 @@ module.exports = ({ config, collections, operations }) => {
try {
const collection = collections[token.collection];
const userQuery = await operations.collections.find({
where: {
email: {
equals: token.email,
const where = {};
if (collection.config.auth.emailVerification) {
where.and = [
{
email: {
equals: token.email,
},
},
},
{
_verified: {
equals: true,
},
},
];
} else {
where.email = {
equals: token.email,
};
}
const userQuery = await operations.collections.find({
where,
collection,
req,
overrideAccess: true,

View File

@@ -4,13 +4,14 @@
const webpack = require('webpack');
const getWebpackProdConfig = require('../webpack/getWebpackProdConfig');
const findConfig = require('../utilities/findConfig');
const getConfig = require('../utilities/getConfig');
const sanitizeConfig = require('../utilities/sanitizeConfig');
module.exports = () => {
const configPath = findConfig();
const configPath = findConfig();
module.exports = () => {
try {
const unsanitizedConfig = require(configPath);
const unsanitizedConfig = getConfig();
const config = sanitizeConfig(unsanitizedConfig);
const webpackProdConfig = getWebpackProdConfig({

View File

@@ -0,0 +1,15 @@
<svg width="260" height="260" viewBox="0 0 260 260" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
path {
fill: #333333;
}
@media (prefers-color-scheme: dark) {
path {
fill: white;
}
}
</style>
<path d="M120.59 8.5824L231.788 75.6142V202.829L148.039 251.418V124.203L36.7866 57.2249L120.59 8.5824Z" />
<path d="M112.123 244.353V145.073L28.2114 193.769L112.123 244.353Z" />
</svg>

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react';
import {
Route, Switch, withRouter, Redirect, useHistory,
} from 'react-router-dom';
import config from 'payload/config';
import { useConfig } from './providers/Config';
import List from './views/collections/List';
import { useUser } from './data/User';
import { useAuthentication } from './providers/Authentication';
import DefaultTemplate from './templates/Default';
import Dashboard from './views/Dashboard';
import ForgotPassword from './views/ForgotPassword';
@@ -20,14 +20,14 @@ import Unauthorized from './views/Unauthorized';
import Account from './views/Account';
import Loading from './elements/Loading';
const {
admin: { user: userSlug }, routes, collections, globals,
} = config;
const Routes = () => {
const history = useHistory();
const [initialized, setInitialized] = useState(null);
const { user, permissions, permissions: { canAccessAdmin } } = useUser();
const { user, permissions, permissions: { canAccessAdmin } } = useAuthentication();
const {
admin: { user: userSlug }, routes, collections, globals,
} = useConfig();
useEffect(() => {
requests.get(`${routes.api}/${userSlug}/init`).then((res) => res.json().then((data) => {
@@ -35,7 +35,7 @@ const Routes = () => {
setInitialized(data.initialized);
}
}));
}, []);
}, [routes, userSlug]);
useEffect(() => {
history.replace();
@@ -95,85 +95,86 @@ const Routes = () => {
<Account />
</Route>
{collections.map((collection) => {
if (permissions?.[collection.slug]?.read?.permission) {
return (
<Route
key={`${collection.slug}-list`}
path={`${match.url}/collections/${collection.slug}`}
exact
render={(routeProps) => (
{collections.map((collection) => (
<Route
key={`${collection.slug}-list`}
path={`${match.url}/collections/${collection.slug}`}
exact
render={(routeProps) => {
if (permissions?.[collection.slug]?.read?.permission) {
return (
<List
{...routeProps}
collection={collection}
/>
)}
/>
);
}
);
}
return null;
})}
return <Unauthorized />;
}}
/>
))}
{collections.map((collection) => {
if (permissions?.[collection.slug]?.create?.permission) {
return (
<Route
key={`${collection.slug}-create`}
path={`${match.url}/collections/${collection.slug}/create`}
exact
render={(routeProps) => (
{collections.map((collection) => (
<Route
key={`${collection.slug}-create`}
path={`${match.url}/collections/${collection.slug}/create`}
exact
render={(routeProps) => {
if (permissions?.[collection.slug]?.create?.permission) {
return (
<Edit
{...routeProps}
collection={collection}
/>
)}
/>
);
}
);
}
return null;
})}
return <Unauthorized />;
}}
/>
))}
{collections.map((collection) => {
if (permissions?.[collection.slug]?.read?.permission) {
return (
<Route
key={`${collection.slug}-edit`}
path={`${match.url}/collections/${collection.slug}/:id`}
exact
render={(routeProps) => (
{collections.map((collection) => (
<Route
key={`${collection.slug}-edit`}
path={`${match.url}/collections/${collection.slug}/:id`}
exact
render={(routeProps) => {
if (permissions?.[collection.slug]?.read?.permission) {
return (
<Edit
isEditing
{...routeProps}
collection={collection}
/>
)}
/>
);
}
);
}
return null;
})}
return <Unauthorized />;
}}
/>
))}
{globals && globals.map((global) => {
if (permissions?.[global.slug]?.read?.permission) {
return (
<Route
key={`${global.slug}`}
path={`${match.url}/globals/${global.slug}`}
exact
render={(routeProps) => (
{globals && globals.map((global) => (
<Route
key={`${global.slug}`}
path={`${match.url}/globals/${global.slug}`}
exact
render={(routeProps) => {
if (permissions?.[global.slug]?.read?.permission) {
return (
<EditGlobal
{...routeProps}
global={global}
/>
)}
/>
);
}
return null;
})}
);
}
return <Unauthorized />;
}}
/>
))}
<Route path={`${match.url}*`}>
<NotFound />

View File

@@ -1,21 +1,23 @@
function stringify(obj) {
const path = require('path');
function stringify(obj, config) {
if (typeof obj === 'object') {
const result = [];
Object.keys(obj).forEach((key) => {
const val = stringify(obj[key]);
const val = stringify(obj[key], config);
if (val !== null) {
result.push(`"${key}": ${val}`);
}
});
return `{${result.join(',')}}`;
}
return `React.lazy(() => import('${obj}'))`;
return `() => import('${path.join(config.paths.configDir, obj)}')`;
}
function recursivelyAddFieldComponents(fields) {
function recursivelyAddFieldComponents(fields, config) {
if (fields) {
return fields.reduce((allFields, field) => {
const subFields = recursivelyAddFieldComponents(field.fields);
const subFields = recursivelyAddFieldComponents(field.fields, config);
if (!field.name && field.fields) {
return {
@@ -24,11 +26,37 @@ function recursivelyAddFieldComponents(fields) {
};
}
if (field.admin.components || field.fields) {
if (field.admin.components || field.fields || field.admin.elements || field.admin.leaves) {
const fieldComponents = {
...(field.admin.components || {}),
};
if (field.admin.elements) {
fieldComponents.elements = {};
field.admin.elements.forEach((element) => {
if (typeof element === 'object') {
fieldComponents.elements[element.name] = {
element: element.element,
button: element.button,
};
}
});
}
if (field.admin.leaves) {
fieldComponents.leaves = {};
field.admin.leaves.forEach((leaf) => {
if (typeof leaf === 'object') {
fieldComponents.leaves[leaf.name] = {
leaf: leaf.leaf,
button: leaf.button,
};
}
});
}
if (field.fields) {
fieldComponents.fields = subFields;
}
@@ -55,7 +83,7 @@ function customComponents(config) {
const newComponents = { ...components };
newComponents[collection.slug] = {
fields: recursivelyAddFieldComponents(collection.fields),
fields: recursivelyAddFieldComponents(collection.fields, config),
...(collection.admin.components || {}),
};
@@ -66,7 +94,7 @@ function customComponents(config) {
const newComponents = { ...globals };
newComponents[global.slug] = {
fields: recursivelyAddFieldComponents(global.fields),
fields: recursivelyAddFieldComponents(global.fields, config),
...(global.admin.components || {}),
};
@@ -77,12 +105,11 @@ function customComponents(config) {
...(allCollectionComponents || {}),
...(allGlobalComponents || {}),
...(config.admin.components || {}),
}).replace(/\\/g, '\\\\');
}, config).replace(/\\/g, '\\\\');
return {
code: `
const React = require('react');
module.exports = ${string};
export default ${string};
`,
};
}

View File

@@ -1,26 +1,37 @@
const getInitialColumnState = (fields, useAsTitle, defaultColumns) => {
let initialColumns = [];
const hasThumbnail = fields.find((field) => field.type === 'thumbnail');
if (Array.isArray(defaultColumns) && defaultColumns.length >= 1) {
return {
columns: defaultColumns,
};
}
if (hasThumbnail) {
initialColumns.push('thumbnail');
}
if (useAsTitle) {
initialColumns.push(useAsTitle);
}
const remainingColumns = fields.filter((field) => field.name !== useAsTitle && field.type !== 'thumbnail')
.slice(0, 3 - initialColumns.length).map((field) => field.name);
const remainingColumns = fields.reduce((remaining, field) => {
if (field.name === useAsTitle) {
return remaining;
}
if (!field.name && Array.isArray(field.fields)) {
return [
...remaining,
...field.fields.map((subField) => subField.name),
];
}
return [
...remaining,
field.name,
];
}, []);
initialColumns = initialColumns.concat(remainingColumns);
initialColumns = initialColumns.slice(0, 4);
return {
columns: initialColumns,

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useReducer } from 'react';
import PropTypes from 'prop-types';
import getInitialState from './getInitialState';
import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields';
import Pill from '../Pill';
import Plus from '../../icons/Plus';
import X from '../../icons/X';
@@ -28,8 +29,8 @@ const reducer = (state, { type, payload }) => {
const ColumnSelector = (props) => {
const {
collection,
collection: {
fields,
admin: {
useAsTitle,
defaultColumns,
@@ -39,6 +40,7 @@ const ColumnSelector = (props) => {
} = props;
const [initialColumns, setInitialColumns] = useState([]);
const [fields] = useState(() => flattenTopLevelFields(collection.fields));
const [columns, dispatchColumns] = useReducer(reducer, initialColumns);
useEffect(() => {

View File

@@ -0,0 +1,112 @@
import React from 'react';
import PropTypes from 'prop-types';
import DatePicker from 'react-datepicker';
import CalendarIcon from '../../icons/Calendar';
import 'react-datepicker/dist/react-datepicker.css';
import './index.scss';
const baseClass = 'date-time-picker';
const DateTime = (props) => {
const {
inputDateTimeFormat,
useDate,
minDate,
maxDate,
monthsShown,
useTime,
minTime,
maxTime,
timeIntervals,
timeFormat,
placeholder: placeholderText,
value,
onChange,
admin: {
readOnly,
} = {},
} = props;
let dateTimeFormat = inputDateTimeFormat;
if (!dateTimeFormat) {
if (useTime && useDate) dateTimeFormat = 'MMM d, yyy h:mma';
else if (useTime) dateTimeFormat = 'h:mma';
else dateTimeFormat = 'MMM d, yyy';
}
const dateTimePickerProps = {
minDate,
maxDate,
dateFormat: dateTimeFormat,
monthsShown: Math.min(2, monthsShown),
showTimeSelect: useTime,
minTime,
maxTime,
timeIntervals,
timeFormat,
placeholderText,
disabled: readOnly,
onChange,
showPopperArrow: false,
selected: value && new Date(value),
customInputRef: 'ref',
};
const classes = [
baseClass,
!useDate && `${baseClass}--hide-dates`,
].filter(Boolean).join(' ');
return (
<div className={classes}>
<div className={`${baseClass}__input-wrapper`}>
<DatePicker {...dateTimePickerProps} />
<CalendarIcon />
</div>
</div>
);
};
DateTime.defaultProps = {
placeholder: undefined,
// date specific props
useDate: true,
minDate: undefined,
maxDate: undefined,
monthsShown: 1,
inputDateTimeFormat: '',
// time specific props
useTime: true,
minTime: undefined,
maxTime: undefined,
timeIntervals: 30,
timeFormat: 'h:mm aa',
value: undefined,
onChange: undefined,
admin: {},
};
DateTime.propTypes = {
placeholder: PropTypes.string,
// date specific props
useDate: PropTypes.bool,
minDate: PropTypes.instanceOf(Date),
maxDate: PropTypes.instanceOf(Date),
monthsShown: PropTypes.number,
inputDateTimeFormat: PropTypes.string,
// time specific props
useTime: PropTypes.bool,
minTime: PropTypes.instanceOf(Date),
maxTime: PropTypes.instanceOf(Date),
timeIntervals: PropTypes.number,
timeFormat: PropTypes.string,
value: PropTypes.instanceOf(Date),
onChange: PropTypes.func,
admin: PropTypes.shape({
readOnly: PropTypes.bool,
}),
};
export default DateTime;

View File

@@ -1,103 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import DatePicker from 'react-datepicker';
import CalendarIcon from '../../icons/Calendar';
import React, { Suspense, lazy } from 'react';
import Loading from '../Loading';
import 'react-datepicker/dist/react-datepicker.css';
import './index.scss';
const DatePicker = lazy(() => import('./DatePicker'));
const baseClass = 'date-time-picker';
const DateTime = (props) => {
const {
inputDateTimeFormat,
useDate,
minDate,
maxDate,
monthsShown,
useTime,
minTime,
maxTime,
timeIntervals,
timeFormat,
placeholder: placeholderText,
value,
onChange,
} = props;
let dateTimeFormat = inputDateTimeFormat;
if (!dateTimeFormat) {
if (useTime && useDate) dateTimeFormat = 'MMM d, yyy h:mma';
else if (useTime) dateTimeFormat = 'h:mma';
else dateTimeFormat = 'MMM d, yyy';
}
const dateTimePickerProps = {
minDate,
maxDate,
dateFormat: dateTimeFormat,
monthsShown: Math.min(2, monthsShown),
showTimeSelect: useTime,
minTime,
maxTime,
timeIntervals,
timeFormat,
placeholderText,
onChange,
showPopperArrow: false,
selected: value && new Date(value),
customInputRef: 'ref',
};
const classes = [
baseClass,
!useDate && `${baseClass}--hide-dates`,
].filter(Boolean).join(' ');
return (
<div className={classes}>
<div className={`${baseClass}__input-wrapper`}>
<DatePicker {...dateTimePickerProps} />
<CalendarIcon />
</div>
</div>
);
};
DateTime.defaultProps = {
placeholder: undefined,
// date specific props
useDate: true,
minDate: undefined,
maxDate: undefined,
monthsShown: 1,
inputDateTimeFormat: '',
// time specific props
useTime: true,
minTime: undefined,
maxTime: undefined,
timeIntervals: 30,
timeFormat: 'h:mm aa',
value: undefined,
};
DateTime.propTypes = {
placeholder: PropTypes.string,
// date specific props
useDate: PropTypes.bool,
minDate: PropTypes.instanceOf(Date),
maxDate: PropTypes.instanceOf(Date),
monthsShown: PropTypes.number,
inputDateTimeFormat: PropTypes.string,
// time specific props
useTime: PropTypes.bool,
minTime: PropTypes.instanceOf(Date),
maxTime: PropTypes.instanceOf(Date),
timeIntervals: PropTypes.number,
timeFormat: PropTypes.string,
value: PropTypes.instanceOf(Date),
onChange: PropTypes.func.isRequired,
};
export default DateTime;
export default (props) => (
<Suspense fallback={<Loading />}>
<DatePicker {...props} />
</Suspense>
);

View File

@@ -3,13 +3,35 @@
$cal-icon-width: 18px;
.date-time-picker {
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box,
.react-datepicker__time-container {
width: 120px;
}
&--hide-dates {
.react-datepicker {
&__month-container {
width: 100%;
&__month-container,
&__navigation--previous,
&__navigation--next {
display: none;
visibility: hidden;
}
&-popper,
&__time-container,
&__time-box {
width: 100%;
}
&__time-container {
.react-datepicker__time {
.react-datepicker__time-box {
width: 100%;
}
}
}
}
}
@@ -69,7 +91,7 @@ $cal-icon-width: 18px;
&--time {
padding: 10px 0;
border-bottom: 1px solid $color-light-gray;
font-weight: bold;
font-weight: 600;
}
}
@@ -111,10 +133,15 @@ $cal-icon-width: 18px;
&__current-month {
padding: 10px 0;
font-weight: 600;
}
&__month-container {
border-right: 1px solid $color-light-gray;
}
&__time-container {
border-left: 1px solid $color-light-gray;
border-left: none;
}
&__day-names {
@@ -131,38 +158,54 @@ $cal-icon-width: 18px;
}
&--selected {
font-weight: 600;
&:focus {
background-color: $color-dark-gray;
background-color: $color-light-gray;
}
}
&--keyboard-selected {
color: white;
font-weight: 600;
&:focus {
background-color: $color-light-gray;
box-shadow: inset 0px 0px 0px 1px $color-dark-gray, 0px 0px 0px 1px $color-dark-gray;
}
}
&--today {
font-weight: 600;
}
}
&__day,
&__day-name {
width: base(1.5);
margin: base(.15);
line-height: base(1.25);
}
}
.react-datepicker-popper {
z-index: 10;
border: 1px solid $color-light-gray;
}
.react-datepicker__day--keyboard-selected,
.react-datepicker__month-text--keyboard-selected,
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected {
background: $color-dark-gray;
box-shadow: inset 0px 0px 0px 1px $color-dark-gray, 0px 0px 0px 1px $color-dark-gray;
background-color: $color-light-gray;
color: $color-dark-gray;
font-weight: normal;
border-radius: 0;
}
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected,
.react-datepicker__day--selected, .react-datepicker__day--in-selecting-range, .react-datepicker__day--in-range, .react-datepicker__month-text--selected, .react-datepicker__month-text--in-selecting-range, .react-datepicker__month-text--in-range {
background: $color-dark-gray;
box-shadow: inset 0px 0px 0px 1px $color-dark-gray, 0px 0px 0px 1px $color-dark-gray;
background-color: $color-light-gray;
color: $color-dark-gray;
border-radius: 0;
}
@@ -171,17 +214,14 @@ $cal-icon-width: 18px;
border-radius: 0;
}
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box,
.react-datepicker__time-container {
width: 120px;
}
.react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button) {
right: 130px;
}
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item {
line-height: 20px;
font-size: base(.5);
font-weight: 500;
}
@include small-break {

View File

@@ -1,8 +1,8 @@
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import config from 'payload/config';
import { useHistory } from 'react-router-dom';
import { Modal, useModal } from '@faceless-ui/modal';
import { useConfig } from '../../providers/Config';
import Button from '../Button';
import MinimalTemplate from '../../templates/Minimal';
import useTitle from '../../../hooks/useTitle';
@@ -11,8 +11,6 @@ import { useStatusList } from '../Status';
import './index.scss';
const { serverURL, routes: { api, admin } } = config;
const baseClass = 'delete-document';
const DeleteDocument = (props) => {
@@ -30,6 +28,7 @@ const DeleteDocument = (props) => {
} = {},
} = props;
const { serverURL, routes: { api, admin } } = useConfig();
const { replaceStatus } = useStatusList();
const [deleting, setDeleting] = useState(false);
const { closeAll, toggle } = useModal();
@@ -78,7 +77,7 @@ const DeleteDocument = (props) => {
return addDefaultError();
}
});
}, [addDefaultError, closeAll, history, id, replaceStatus, singular, slug, title]);
}, [addDefaultError, closeAll, history, id, replaceStatus, singular, slug, title, admin, api, serverURL]);
if (id) {
return (

View File

@@ -1,19 +1,18 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import config from 'payload/config';
import { useConfig } from '../../providers/Config';
import Button from '../Button';
import { useForm } from '../../forms/Form/context';
import './index.scss';
const { routes: { admin } } = config;
const baseClass = 'duplicate';
const Duplicate = ({ slug }) => {
const { push } = useHistory();
const { getData } = useForm();
const { routes: { admin } } = useConfig();
const handleClick = useCallback(() => {
const data = getData();
@@ -24,7 +23,7 @@ const Duplicate = ({ slug }) => {
data,
},
});
}, [push, getData, slug]);
}, [push, getData, slug, admin]);
return (
<Button

View File

@@ -1,13 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import config from '../../../../config';
import { useConfig } from '../../../providers/Config';
import CopyToClipboard from '../../CopyToClipboard';
import formatFilesize from '../../../../../uploads/formatFilesize';
import './index.scss';
const { serverURL } = config;
const baseClass = 'file-meta';
const Meta = (props) => {
@@ -15,6 +13,8 @@ const Meta = (props) => {
filename, filesize, width, height, mimeType, staticURL,
} = props;
const { serverURL } = useConfig();
const fileURL = `${serverURL}${staticURL}/${filename}`;
return (
@@ -35,18 +35,18 @@ const Meta = (props) => {
<div className={`${baseClass}__size-type`}>
{formatFilesize(filesize)}
{(width && height) && (
<>
<React.Fragment>
&nbsp;-&nbsp;
{width}
x
{height}
</>
</React.Fragment>
)}
{mimeType && (
<>
<React.Fragment>
&nbsp;-&nbsp;
{mimeType}
</>
</React.Fragment>
)}
</div>
</div>

View File

@@ -18,6 +18,8 @@ const FileDetails = (props) => {
const [moreInfoOpen, setMoreInfoOpen] = useState(false);
const hasSizes = sizes && Object.keys(sizes)?.length > 0;
return (
<div className={baseClass}>
<header>
@@ -34,23 +36,23 @@ const FileDetails = (props) => {
height={height}
mimeType={mimeType}
/>
{sizes && (
{hasSizes && (
<Button
className={`${baseClass}__toggle-more-info${moreInfoOpen ? ' open' : ''}`}
buttonStyle="none"
onClick={() => setMoreInfoOpen(!moreInfoOpen)}
>
{!moreInfoOpen && (
<>
<React.Fragment>
More info
<Chevron />
</>
</React.Fragment>
)}
{moreInfoOpen && (
<>
<React.Fragment>
Less info
<Chevron />
</>
</React.Fragment>
)}
</Button>
)}
@@ -66,26 +68,24 @@ const FileDetails = (props) => {
/>
)}
</header>
{sizes && (
{hasSizes && (
<AnimateHeight
className={`${baseClass}__more-info`}
height={moreInfoOpen ? 'auto' : 0}
>
<ul className={`${baseClass}__sizes`}>
{Object.entries(sizes).map(([key, val]) => {
return (
<li key={key}>
<div className={`${baseClass}__size-label`}>
{key}
</div>
<Meta
{...val}
mimeType={mimeType}
staticURL={staticURL}
/>
</li>
);
})}
{Object.entries(sizes).map(([key, val]) => (
<li key={key}>
<div className={`${baseClass}__size-label`}>
{key}
</div>
<Meta
{...val}
mimeType={mimeType}
staticURL={staticURL}
/>
</li>
))}
</ul>
</AnimateHeight>
)}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';
import qs from 'qs';
import config from 'payload/config';
import { useConfig } from '../../providers/Config';
import { useLocale } from '../../utilities/Locale';
import { useSearchParams } from '../../utilities/SearchParams';
import Popup from '../Popup';
@@ -10,9 +10,8 @@ import './index.scss';
const baseClass = 'localizer';
const { localization } = config;
const Localizer = () => {
const { localization } = useConfig();
const locale = useLocale();
const searchParams = useSearchParams();
@@ -24,48 +23,46 @@ const Localizer = () => {
<Popup
align="left"
button={locale}
render={({ close }) => {
return (
<div>
<span>Locales</span>
<ul>
{locales.map((localeOption) => {
const baseLocaleClass = `${baseClass}__locale`;
render={({ close }) => (
<div>
<span>Locales</span>
<ul>
{locales.map((localeOption) => {
const baseLocaleClass = `${baseClass}__locale`;
const localeClasses = [
baseLocaleClass,
locale === localeOption && `${baseLocaleClass}--active`,
];
const localeClasses = [
baseLocaleClass,
locale === localeOption && `${baseLocaleClass}--active`,
];
const newParams = {
...searchParams,
locale: localeOption,
};
const newParams = {
...searchParams,
locale: localeOption,
};
const search = qs.stringify(newParams);
const search = qs.stringify(newParams);
if (localeOption !== locale) {
return (
<li
key={localeOption}
className={localeClasses}
if (localeOption !== locale) {
return (
<li
key={localeOption}
className={localeClasses}
>
<Link
to={{ search }}
onClick={close}
>
<Link
to={{ search }}
onClick={close}
>
{localeOption}
</Link>
</li>
);
}
{localeOption}
</Link>
</li>
);
}
return null;
})}
</ul>
</div>
);
}}
return null;
})}
</ul>
</div>
)}
/>
</div>
);

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { NavLink, Link, useHistory } from 'react-router-dom';
import config from 'payload/config';
import { useUser } from '../../data/User';
import { useConfig } from '../../providers/Config';
import { useAuthentication } from '../../providers/Authentication';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
import Chevron from '../../icons/Chevron';
import LogOut from '../../icons/LogOut';
import Menu from '../../icons/Menu';
@@ -14,18 +15,17 @@ import './index.scss';
const baseClass = 'nav';
const {
collections,
globals,
routes: {
admin,
},
} = config;
const Nav = () => {
const { permissions } = useUser();
const DefaultNav = () => {
const { permissions } = useAuthentication();
const [menuActive, setMenuActive] = useState(false);
const history = useHistory();
const {
collections,
globals,
routes: {
admin,
},
} = useConfig();
const classes = [
baseClass,
@@ -34,7 +34,7 @@ const Nav = () => {
useEffect(() => history.listen(() => {
setMenuActive(false);
}), []);
}), [history]);
return (
<aside className={classes}>
@@ -127,4 +127,21 @@ const Nav = () => {
);
};
const Nav = () => {
const {
admin: {
components: {
nav: CustomNav,
} = {}
} = {},
} = useConfig();
return (
<RenderCustomComponent
CustomComponent={CustomNav}
DefaultComponent={DefaultNav}
/>
);
}
export default Nav;

View File

@@ -29,7 +29,7 @@
outline: 0;
padding: base(.5);
color: $color-dark-gray;
line-height: 1;
line-height: base(1);
&:hover:not(.clickable-arrow--is-disabled) {
background: $color-background-gray;

View File

@@ -26,7 +26,8 @@
&__scroll {
padding: base(1);
overflow-y: scroll;
overflow-y: auto;
width: calc(100% + var(--scrollbar-width));
white-space: nowrap;
}

View File

@@ -1,13 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useForm } from '../../forms/Form/context';
import { useUser } from '../../data/User';
import { useAuthentication } from '../../providers/Authentication';
import Button from '../Button';
const baseClass = 'preview-btn';
const PreviewButton = ({ generatePreviewURL }) => {
const { token } = useUser();
const { token } = useAuthentication();
const { getFields } = useForm();
const fields = getFields();

View File

@@ -51,6 +51,7 @@ div.react-select {
}
.rs__menu {
z-index: 2;
border-radius: 0;
@include inputShadowActive;
}

View File

@@ -16,29 +16,23 @@ const Table = ({ columns, data }) => {
>
<thead>
<tr>
{columns.map((col, i) => {
return (
<th key={i}>
{col.components.Heading}
</th>
);
})}
{columns.map((col, i) => (
<th key={i}>
{col.components.Heading}
</th>
))}
</tr>
</thead>
<tbody>
{data && data.map((row, rowIndex) => {
return (
<tr key={rowIndex}>
{columns.map((col, colIndex) => {
return (
<td key={colIndex}>
{col.components.renderCell(row, row[col.accessor])}
</td>
);
})}
</tr>
);
})}
{data && data.map((row, rowIndex) => (
<tr key={rowIndex}>
{columns.map((col, colIndex) => (
<td key={colIndex}>
{col.components.renderCell(row, row[col.accessor])}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>

View File

@@ -1,13 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import config from '../../../config';
import { useConfig } from '../../providers/Config';
import FileGraphic from '../../graphics/File';
import getThumbnail from '../../../../uploads/getThumbnail';
import './index.scss';
const { serverURL } = config;
const baseClass = 'thumbnail';
const Thumbnail = (props) => {
@@ -15,6 +13,8 @@ const Thumbnail = (props) => {
filename, mimeType, staticURL, sizes, adminThumbnail, size,
} = props;
const { serverURL } = useConfig();
const thumbnail = getThumbnail(mimeType, staticURL, filename, sizes, adminThumbnail);
const classes = [

View File

@@ -25,7 +25,6 @@ const Condition = (props) => {
value,
orIndex,
andIndex,
collectionSlug,
} = props;
const [activeField, setActiveField] = useState({ operators: [] });
@@ -33,7 +32,7 @@ const Condition = (props) => {
const debouncedValue = useDebounce(internalValue, 300);
useEffect(() => {
const newActiveField = fields.find(field => value.field === field.value);
const newActiveField = fields.find((field) => value.field === field.value);
if (newActiveField) {
setActiveField(newActiveField);
@@ -57,9 +56,9 @@ const Condition = (props) => {
<div className={`${baseClass}__inputs`}>
<div className={`${baseClass}__field`}>
<ReactSelect
value={fields.find(field => value.field === field.value)}
value={fields.find((field) => value.field === field.value)}
options={fields}
onChange={field => dispatch({
onChange={(field) => dispatch({
type: 'update',
orIndex,
andIndex,
@@ -69,9 +68,9 @@ const Condition = (props) => {
</div>
<div className={`${baseClass}__operator`}>
<ReactSelect
value={activeField.operators.find(operator => value.operator === operator.value)}
value={activeField.operators.find((operator) => value.operator === operator.value)}
options={activeField.operators}
onChange={operator => dispatch({
onChange={(operator) => dispatch({
type: 'update',
orIndex,
andIndex,
@@ -81,7 +80,7 @@ const Condition = (props) => {
</div>
<div className={`${baseClass}__value`}>
<RenderCustomComponent
path={`${collectionSlug}.fields.${activeField.value}.filter`}
CustomComponent={activeField?.props?.admin?.components?.filter}
DefaultComponent={ValueComponent}
componentProps={{
...activeField.props,
@@ -144,7 +143,6 @@ Condition.propTypes = {
dispatch: PropTypes.func.isRequired,
orIndex: PropTypes.number.isRequired,
andIndex: PropTypes.number.isRequired,
collectionSlug: PropTypes.string.isRequired,
};
export default Condition;

View File

@@ -1,10 +1,11 @@
import React, { useState, useEffect, useReducer } from 'react';
import React, { useState, useReducer } from 'react';
import PropTypes from 'prop-types';
import useThrottledEffect from '../../../hooks/useThrottledEffect';
import Button from '../Button';
import reducer from './reducer';
import Condition from './Condition';
import fieldTypes from './field-types';
import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields';
import './index.scss';
@@ -18,11 +19,30 @@ const validateWhereQuery = (query) => {
return null;
};
const reduceFields = (fields) => flattenTopLevelFields(fields).reduce((reduced, field) => {
if (typeof fieldTypes[field.type] === 'object') {
const formattedField = {
label: field.label,
value: field.name,
...fieldTypes[field.type],
props: {
...field,
},
};
return [
...reduced,
formattedField,
];
}
return reduced;
}, []);
const WhereBuilder = (props) => {
const {
collection,
collection: {
fields,
slug,
labels: {
plural,
} = {},
@@ -31,29 +51,7 @@ const WhereBuilder = (props) => {
} = props;
const [where, dispatchWhere] = useReducer(reducer, []);
const [reducedFields, setReducedFields] = useState([]);
useEffect(() => {
setReducedFields(fields.reduce((reduced, field) => {
if (typeof fieldTypes[field.type] === 'object') {
const formattedField = {
label: field.label,
value: field.name,
...fieldTypes[field.type],
props: {
...field,
},
};
return [
...reduced,
formattedField,
];
}
return reduced;
}, []));
}, [fields]);
const [reducedFields] = useState(() => reduceFields(collection.fields));
useThrottledEffect(() => {
let whereQuery = {
@@ -61,27 +59,25 @@ const WhereBuilder = (props) => {
};
if (where) {
whereQuery.or = where.map((or) => {
return or.reduce((conditions, condition) => {
const { field, operator, value } = condition;
if (field && operator && value) {
return {
and: [
...conditions.and,
{
[field]: {
[operator]: value,
},
whereQuery.or = where.map((or) => or.reduce((conditions, condition) => {
const { field, operator, value } = condition;
if (field && operator && value) {
return {
and: [
...conditions.and,
{
[field]: {
[operator]: value,
},
],
};
}
},
],
};
}
return conditions;
}, {
and: [],
});
});
return conditions;
}, {
and: [],
}));
}
whereQuery = validateWhereQuery(whereQuery);
@@ -92,7 +88,7 @@ const WhereBuilder = (props) => {
return (
<div className={baseClass}>
{where.length > 0 && (
<>
<React.Fragment>
<div className={`${baseClass}__label`}>
Filter
{' '}
@@ -101,39 +97,34 @@ const WhereBuilder = (props) => {
where
</div>
<ul className={`${baseClass}__or-filters`}>
{where.map((or, orIndex) => {
return (
<li key={orIndex}>
{orIndex !== 0 && (
<div className={`${baseClass}__label`}>
Or
</div>
)}
<ul className={`${baseClass}__and-filters`}>
{or && or.map((_, andIndex) => {
return (
<li key={andIndex}>
{andIndex !== 0 && (
<div className={`${baseClass}__label`}>
And
</div>
)}
<Condition
collectionSlug={slug}
value={where[orIndex][andIndex]}
orIndex={orIndex}
andIndex={andIndex}
key={andIndex}
fields={reducedFields}
dispatch={dispatchWhere}
/>
</li>
);
})}
</ul>
</li>
);
})}
{where.map((or, orIndex) => (
<li key={orIndex}>
{orIndex !== 0 && (
<div className={`${baseClass}__label`}>
Or
</div>
)}
<ul className={`${baseClass}__and-filters`}>
{or && or.map((_, andIndex) => (
<li key={andIndex}>
{andIndex !== 0 && (
<div className={`${baseClass}__label`}>
And
</div>
)}
<Condition
value={where[orIndex][andIndex]}
orIndex={orIndex}
andIndex={andIndex}
key={andIndex}
fields={reducedFields}
dispatch={dispatchWhere}
/>
</li>
))}
</ul>
</li>
))}
</ul>
<Button
className={`${baseClass}__add-or`}
@@ -145,7 +136,7 @@ const WhereBuilder = (props) => {
>
Or
</Button>
</>
</React.Fragment>
)}
{where.length === 0 && (
<div className={`${baseClass}__no-filters`}>
@@ -169,7 +160,6 @@ const WhereBuilder = (props) => {
WhereBuilder.propTypes = {
handleChange: PropTypes.func.isRequired,
collection: PropTypes.shape({
slug: PropTypes.string,
fields: PropTypes.arrayOf(
PropTypes.shape({}),
),

View File

@@ -23,95 +23,89 @@ const ActionPanel = (props) => {
const classes = [
baseClass,
`${baseClass}--vertical-alignment-${verticalAlignment}`,
].filter(Boolean).join(' ');
return (
<div className={classes}>
<div className={`${baseClass}__controls-container`}>
<div className={`${baseClass}__controls`}>
<Popup
showOnHover
size="wide"
color="dark"
horizontalAlign="center"
buttonType="custom"
button={(
<Button
className={`${baseClass}__remove-row`}
round
buttonStyle="none"
icon="x"
iconPosition="left"
iconStyle="with-border"
onClick={removeRow}
/>
)}
>
Remove&nbsp;
{singularLabel}
</Popup>
{blockType === 'blocks'
? (
<Popup
buttonType="custom"
size="large"
horizontalAlign="right"
button={(
<Button
className={`${baseClass}__add-row`}
round
buttonStyle="none"
icon="plus"
iconPosition="left"
iconStyle="with-border"
/>
)}
render={({ close }) => (
<BlockSelector
blocks={blocks}
addRow={addRow}
addRowIndex={rowIndex}
close={close}
parentIsHovered={isHovered}
watchParentHover
/>
)}
/>
)
: (
<Popup
showOnHover
size="wide"
color="dark"
horizontalAlign="center"
horizontalAlign="right"
buttonType="custom"
button={(
<Button
className={`${baseClass}__remove-row`}
className={`${baseClass}__add-row`}
round
buttonStyle="none"
icon="x"
icon="plus"
iconPosition="left"
iconStyle="with-border"
onClick={removeRow}
onClick={addRow}
/>
)}
>
Remove&nbsp;
Add&nbsp;
{singularLabel}
</Popup>
{blockType === 'blocks'
? (
<Popup
buttonType="custom"
size="large"
horizontalAlign="right"
button={(
<Button
className={`${baseClass}__add-row`}
round
buttonStyle="none"
icon="plus"
iconPosition="left"
iconStyle="with-border"
/>
)}
render={({ close }) => (
<BlockSelector
blocks={blocks}
addRow={addRow}
addRowIndex={rowIndex}
close={close}
parentIsHovered={isHovered}
watchParentHover
/>
)}
/>
)
: (
<Popup
showOnHover
size="wide"
color="dark"
horizontalAlign="right"
buttonType="custom"
button={(
<Button
className={`${baseClass}__add-row`}
round
buttonStyle="none"
icon="plus"
iconPosition="left"
iconStyle="with-border"
onClick={addRow}
/>
)}
>
Add&nbsp;
{singularLabel}
</Popup>
)}
</div>
</div>
)}
</div>
);
};
ActionPanel.defaultProps = {
singularLabel: 'Row',
verticalAlignment: 'center',
blockType: null,
isHovered: false,
blocks: [],
@@ -125,7 +119,6 @@ ActionPanel.propTypes = {
blocks: PropTypes.arrayOf(
PropTypes.shape({}),
),
verticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']),
isHovered: PropTypes.bool,
rowIndex: PropTypes.number.isRequired,
};

View File

@@ -1,42 +1,6 @@
@import '../../../../scss/styles';
.action-panel {
padding: 0 base(.5);
padding-left: base(.75);
margin-bottom: base(1);
&:hover {
z-index: $z-nav;
}
&__controls-container {
position: relative;
height: 100%;
}
&__controls {
display: flex;
justify-content: center;
height: 100%;
flex-direction: column;
color: $color-gray;
opacity: 0;
visibility: hidden;
}
&--vertical-alignment-top {
.action-panel__controls {
justify-content: flex-start;
}
}
&--vertical-alignment-sticky {
.action-panel__controls {
position: sticky;
top: 120px;
height: unset;
}
}
&__remove-row {
margin: 0 0 base(.3);
@@ -45,17 +9,4 @@
&__add-row {
margin: base(.3) 0 0;
}
@include mid-break {
&__controls {
opacity: 1;
visibility: visible;
}
&--vertical-alignment-sticky {
.action-panel__controls {
top: 100px;
}
}
}
}

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