Compare commits
307 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3feec809e6 | ||
|
|
8dd3298f9a | ||
|
|
4c11134141 | ||
|
|
45c824c042 | ||
|
|
181d27e33d | ||
|
|
0e225e1ab1 | ||
|
|
5676f22ede | ||
|
|
e9cc178ed7 | ||
|
|
09a3c859c1 | ||
|
|
628d68ed2d | ||
|
|
fe99952561 | ||
|
|
9c28086ba4 | ||
|
|
f8c225316b | ||
|
|
df1f381354 | ||
|
|
939a2923d3 | ||
|
|
0e15f8376c | ||
|
|
c9d9d5269d | ||
|
|
97f8f2b749 | ||
|
|
ea21480230 | ||
|
|
2918bbeeca | ||
|
|
ac1aea23dc | ||
|
|
d478bd0e34 | ||
|
|
1695e4e685 | ||
|
|
20bf4680db | ||
|
|
4f3dd59d25 | ||
|
|
dde3a35b16 | ||
|
|
a964860e55 | ||
|
|
41dcdabe9c | ||
|
|
8ad96452a0 | ||
|
|
9f426913a2 | ||
|
|
e8376a5662 | ||
|
|
08dcd7f8b2 | ||
|
|
03e1b730b5 | ||
|
|
8252c409d4 | ||
|
|
70ae5b1a19 | ||
|
|
0d42060882 | ||
|
|
e35b212781 | ||
|
|
08731594cb | ||
|
|
c439297223 | ||
|
|
88d80aec55 | ||
|
|
6f6289a069 | ||
|
|
af65787862 | ||
|
|
a39cec2b76 | ||
|
|
f20feae4d9 | ||
|
|
2d1169ac66 | ||
|
|
966dd7a40f | ||
|
|
eee835696c | ||
|
|
e95edbd3e4 | ||
|
|
b462fe5f77 | ||
|
|
a1865ed5fb | ||
|
|
209d812239 | ||
|
|
4e3cc2bc05 | ||
|
|
f5731f2fc8 | ||
|
|
29b608d6f8 | ||
|
|
a10dd738c0 | ||
|
|
0fb96ff3e9 | ||
|
|
55488c5c2e | ||
|
|
5345eb993a | ||
|
|
8f67ee4ac3 | ||
|
|
9f8b1d9521 | ||
|
|
d4c4418a3e | ||
|
|
bb5f747052 | ||
|
|
f2f8ffbbb2 | ||
|
|
fab4c0ed76 | ||
|
|
7b3cefd4ef | ||
|
|
ad483da837 | ||
|
|
01d49ccd36 | ||
|
|
7ceadc15a3 | ||
|
|
3caf81d9f7 | ||
|
|
dd6740e109 | ||
|
|
28e4cf6726 | ||
|
|
95e38a568f | ||
|
|
43f1f41179 | ||
|
|
698a9ead04 | ||
|
|
bd3f628d55 | ||
|
|
14d702ff8d | ||
|
|
dd5acae76f | ||
|
|
9de51bbf02 | ||
|
|
9c44a159b9 | ||
|
|
43e3162c7d | ||
|
|
2b81e887ad | ||
|
|
bfd492b7ed | ||
|
|
bf452c23bc | ||
|
|
da3495cb4b | ||
|
|
899a158843 | ||
|
|
163daf5816 | ||
|
|
7fbab16b22 | ||
|
|
17b8335c3c | ||
|
|
93930d1125 | ||
|
|
9cb630f98d | ||
|
|
f695bd8c1d | ||
|
|
8a8222995f | ||
|
|
ca24263c17 | ||
|
|
0bf0dd7d5e | ||
|
|
ef3a70cc38 | ||
|
|
6fe09c8264 | ||
|
|
4616e7fa16 | ||
|
|
d99788f6f7 | ||
|
|
8a1bd762ba | ||
|
|
c760863f8c | ||
|
|
ed5c3b55f1 | ||
|
|
7d0e2820cc | ||
|
|
f381b9261a | ||
|
|
01f170ba73 | ||
|
|
a53e24904f | ||
|
|
423cdbd63f | ||
|
|
e38c0e687f | ||
|
|
9e82f9d02e | ||
|
|
5f9c48494a | ||
|
|
155db092c4 | ||
|
|
87522ff7d6 | ||
|
|
e6b435e88e | ||
|
|
8c78a0a773 | ||
|
|
597f7d3075 | ||
|
|
9da8aca015 | ||
|
|
13aa1eb13d | ||
|
|
918228c9dc | ||
|
|
92a25867ff | ||
|
|
f698594137 | ||
|
|
c15dff4afb | ||
|
|
985db67d48 | ||
|
|
30c5eeaff9 | ||
|
|
fd81085b54 | ||
|
|
9955621f0a | ||
|
|
599a693f5b | ||
|
|
da6a9a2ba7 | ||
|
|
7002752744 | ||
|
|
53ed79ecbc | ||
|
|
1e342f8bee | ||
|
|
d19576978b | ||
|
|
af59822510 | ||
|
|
f45f93ce38 | ||
|
|
0d1ffdcb4b | ||
|
|
b197d780c1 | ||
|
|
45da1369ca | ||
|
|
478d1cf5ee | ||
|
|
ec0b2c6a5b | ||
|
|
3a5881cc48 | ||
|
|
13c857f753 | ||
|
|
5091a40e96 | ||
|
|
1cf45f7fa9 | ||
|
|
b49b531ce4 | ||
|
|
7553e14a8b | ||
|
|
cb984f4d54 | ||
|
|
9630c2c3b2 | ||
|
|
8bfc892f7c | ||
|
|
756d275edf | ||
|
|
bfe7a139af | ||
|
|
95352f47ea | ||
|
|
98a9eb8d23 | ||
|
|
9a62c2ab5d | ||
|
|
9c928d497c | ||
|
|
6f777dd09c | ||
|
|
5777868eba | ||
|
|
063b3b86c8 | ||
|
|
9ef9cca948 | ||
|
|
74ac23dbc6 | ||
|
|
b2145a0a59 | ||
|
|
fe307ecfeb | ||
|
|
80b99653cf | ||
|
|
75aa92952a | ||
|
|
3375963449 | ||
|
|
f91c47bb37 | ||
|
|
60552d9d86 | ||
|
|
857cf088f6 | ||
|
|
5c506a1db7 | ||
|
|
b81c7917a9 | ||
|
|
5802011eea | ||
|
|
c91f4d9e0d | ||
|
|
b5690cf721 | ||
|
|
aaee4d16d1 | ||
|
|
5148a21769 | ||
|
|
ec461e2440 | ||
|
|
ada2b79054 | ||
|
|
3d652b2c9a | ||
|
|
63bc00e8da | ||
|
|
8162b1fc93 | ||
|
|
a009387461 | ||
|
|
8dbf10dc4a | ||
|
|
954cd9437b | ||
|
|
1c532d3719 | ||
|
|
273351103d | ||
|
|
fd15b06773 | ||
|
|
9efb5e881b | ||
|
|
e674bb6779 | ||
|
|
8df93b23b5 | ||
|
|
afc47d92b5 | ||
|
|
d47c8a240a | ||
|
|
9be27927c1 | ||
|
|
e0c1400ee4 | ||
|
|
eb604a566d | ||
|
|
0b2add2e1a | ||
|
|
1ca046532d | ||
|
|
c1843170d6 | ||
|
|
85bf65e40e | ||
|
|
2a2494366b | ||
|
|
b53dbc1a27 | ||
|
|
6c881e3c48 | ||
|
|
17b4251176 | ||
|
|
c7411fd347 | ||
|
|
f32e7f8f4c | ||
|
|
2667c6fa98 | ||
|
|
46309f33a2 | ||
|
|
fb495d7b04 | ||
|
|
f8840ee8b7 | ||
|
|
3c025377f5 | ||
|
|
a01b308c38 | ||
|
|
53c56c537d | ||
|
|
a669ca7099 | ||
|
|
258d4cd62e | ||
|
|
fe2c1e69e5 | ||
|
|
a77dcaa6c2 | ||
|
|
dfd4f75c98 | ||
|
|
322b726be8 | ||
|
|
2978509034 | ||
|
|
d9c1b41e08 | ||
|
|
9538af2e92 | ||
|
|
2eb19876ee | ||
|
|
74fb5397ac | ||
|
|
0630c006d5 | ||
|
|
71a5a525d1 | ||
|
|
5ba0313b8f | ||
|
|
7a775166b2 | ||
|
|
9edf4ae2da | ||
|
|
fcef33ad1f | ||
|
|
cbf18d75a7 | ||
|
|
6f38e5c264 | ||
|
|
ead0568a94 | ||
|
|
5b5acd7471 | ||
|
|
87e79817f7 | ||
|
|
2b969b91b9 | ||
|
|
609bf54c45 | ||
|
|
9a104b5d12 | ||
|
|
65eccb129f | ||
|
|
022bf17b59 | ||
|
|
ae7583401e | ||
|
|
1ca904f164 | ||
|
|
bc884e64dd | ||
|
|
89ceff24fc | ||
|
|
47b2940358 | ||
|
|
facff24abc | ||
|
|
8f10f14bc4 | ||
|
|
3c522930ee | ||
|
|
0da92c20ea | ||
|
|
43db6a491d | ||
|
|
ae77d68f72 | ||
|
|
0e76d3e79d | ||
|
|
f0ae33f780 | ||
|
|
684e23fa2f | ||
|
|
bf2ed1747c | ||
|
|
90a09e268f | ||
|
|
0cc468ad6a | ||
|
|
16b9aab785 | ||
|
|
749388e877 | ||
|
|
17dfa693ba | ||
|
|
2dcb80e681 | ||
|
|
26ab75a356 | ||
|
|
97c31faea1 | ||
|
|
8b167e9a9e | ||
|
|
d9bf8a5fcc | ||
|
|
7b5dd0ac5f | ||
|
|
210295e704 | ||
|
|
ed56971de6 | ||
|
|
298948b203 | ||
|
|
f5b55ddbb4 | ||
|
|
99d083bb09 | ||
|
|
455f2ab183 | ||
|
|
f4127e1820 | ||
|
|
8fa95b7851 | ||
|
|
aefdabfd7e | ||
|
|
470a33f7f9 | ||
|
|
d179427d7b | ||
|
|
599af3f924 | ||
|
|
6e383d9005 | ||
|
|
d74c1e3b93 | ||
|
|
c1814cbc3f | ||
|
|
c0fef9bfde | ||
|
|
1363bec776 | ||
|
|
f3972430f6 | ||
|
|
998cb656a5 | ||
|
|
1a8228c08b | ||
|
|
07f9a06571 | ||
|
|
9c8145238c | ||
|
|
3445f5bdce | ||
|
|
8d3573d734 | ||
|
|
1fc32fa0bb | ||
|
|
255f6e6215 | ||
|
|
1aa649782b | ||
|
|
16d2f400c4 | ||
|
|
4d99594021 | ||
|
|
790c85df5a | ||
|
|
0a98778ec4 | ||
|
|
cc4794e5ac | ||
|
|
e301efdb7b | ||
|
|
a4cf98c460 | ||
|
|
c9e37bcef5 | ||
|
|
deaf5cde5f | ||
|
|
d38b8e746c | ||
|
|
1be0890f03 | ||
|
|
d45ff316c0 | ||
|
|
d2055141be | ||
|
|
846589e303 | ||
|
|
8efc03b325 | ||
|
|
28d8c10a3e | ||
|
|
cddba8d9e7 | ||
|
|
fa02f00503 | ||
|
|
9a095f574b |
@@ -11,5 +11,6 @@ module.exports = {
|
||||
]
|
||||
}
|
||||
],
|
||||
'no-underscore-dangle': 'off',
|
||||
},
|
||||
};
|
||||
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto eol=lf
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
25
.vscode/launch.json
vendored
@@ -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",
|
||||
|
||||
17
README.md
17
README.md
@@ -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
3
admin/components.js
Normal 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
1
admin/elements.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Button } from '../src/client/components/elements/Button';
|
||||
6
admin/forms.js
Normal file
6
admin/forms.js
Normal 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
1
admin/icons.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as X } from '../src/client/components/icons/X';
|
||||
4
admin/rich-text.js
Normal file
4
admin/rich-text.js
Normal 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
1
admin/styles.scss
Normal file
@@ -0,0 +1 @@
|
||||
@import '../src/client/scss/styles.scss';
|
||||
21
babel.config.js
Normal file
21
babel.config.js
Normal 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'),
|
||||
],
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import PageList from '../collections/Page/components/List';
|
||||
|
||||
const components = {
|
||||
pages: {
|
||||
List: PageList,
|
||||
},
|
||||
};
|
||||
|
||||
export default components;
|
||||
104
demo/client/components/richText/elements/Button/Button/index.js
Normal file
104
demo/client/components/richText/elements/Button/Button/index.js
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
10
demo/client/components/richText/elements/Button/index.js
Normal file
10
demo/client/components/richText/elements/Button/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const button = require('./Button');
|
||||
const element = require('./Element');
|
||||
const plugin = require('./plugin');
|
||||
|
||||
module.exports = {
|
||||
name: 'button',
|
||||
button,
|
||||
element,
|
||||
plugin,
|
||||
};
|
||||
10
demo/client/components/richText/elements/Button/plugin.js
Normal file
10
demo/client/components/richText/elements/Button/plugin.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,8 @@
|
||||
const button = require('./Button');
|
||||
const leaf = require('./Leaf');
|
||||
|
||||
module.exports = {
|
||||
name: 'purple-background',
|
||||
button,
|
||||
leaf,
|
||||
};
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -10,6 +10,9 @@ const Code = {
|
||||
type: 'code',
|
||||
label: 'Code',
|
||||
required: true,
|
||||
admin: {
|
||||
language: 'js',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
36
demo/collections/LocalOperations.js
Normal file
36
demo/collections/LocalOperations.js
Normal 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;
|
||||
@@ -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',
|
||||
|
||||
@@ -12,7 +12,7 @@ module.exports = {
|
||||
},
|
||||
upload: {
|
||||
staticURL: '/media',
|
||||
staticDir: path.resolve(__dirname, '../media'),
|
||||
staticDir: './media',
|
||||
adminThumbnail: 'mobile',
|
||||
imageSizes: [
|
||||
{
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
43
demo/collections/Select.js
Normal file
43
demo/collections/Select.js
Normal 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
15
demo/custom-index.html
Normal 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>
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
3
logger.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const logger = require('./src/utilities/logger');
|
||||
|
||||
module.exports = logger;
|
||||
32
package.json
32
package.json
@@ -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
170
payload.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -10,14 +10,4 @@ module.exports = [
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'resetPasswordToken',
|
||||
type: 'text',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
name: 'resetPasswordExpiration',
|
||||
type: 'date',
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -10,6 +10,7 @@ const defaultUser = {
|
||||
auth: {
|
||||
tokenExpiration: 7200,
|
||||
},
|
||||
fields: [],
|
||||
timestamps: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -8,6 +8,7 @@ function resetPassword(collection) {
|
||||
collection,
|
||||
data: args,
|
||||
req: context.req,
|
||||
res: context.res,
|
||||
api: 'GraphQL',
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
22
src/auth/operations/local/forgotPassword.js
Normal file
22
src/auth/operations/local/forgotPassword.js
Normal 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;
|
||||
9
src/auth/operations/local/index.js
Normal file
9
src/auth/operations/local/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const login = require('./login');
|
||||
const forgotPassword = require('./forgotPassword');
|
||||
const resetPassword = require('./resetPassword');
|
||||
|
||||
module.exports = {
|
||||
login,
|
||||
forgotPassword,
|
||||
resetPassword,
|
||||
};
|
||||
33
src/auth/operations/local/login.js
Normal file
33
src/auth/operations/local/login.js
Normal 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;
|
||||
24
src/auth/operations/local/resetPassword.js
Normal file
24
src/auth/operations/local/resetPassword.js
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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;
|
||||
26
src/auth/operations/verifyEmail.js
Normal file
26
src/auth/operations/verifyEmail.js
Normal 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;
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
19
src/auth/requestHandlers/verifyEmail.js
Normal file
19
src/auth/requestHandlers/verifyEmail.js
Normal 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;
|
||||
34
src/auth/sendVerificationEmail.js
Normal file
34
src/auth/sendVerificationEmail.js
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
15
src/client/assets/images/favicon.svg
Normal file
15
src/client/assets/images/favicon.svg
Normal 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 |
BIN
src/client/assets/images/og-image.png
Normal file
BIN
src/client/assets/images/og-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
@@ -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 />
|
||||
|
||||
@@ -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};
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
112
src/client/components/elements/DatePicker/DatePicker.js
Normal file
112
src/client/components/elements/DatePicker/DatePicker.js
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
-
|
||||
{width}
|
||||
x
|
||||
{height}
|
||||
</>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{mimeType && (
|
||||
<>
|
||||
<React.Fragment>
|
||||
-
|
||||
{mimeType}
|
||||
</>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
|
||||
&__scroll {
|
||||
padding: base(1);
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
width: calc(100% + var(--scrollbar-width));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ div.react-select {
|
||||
}
|
||||
|
||||
.rs__menu {
|
||||
z-index: 2;
|
||||
border-radius: 0;
|
||||
@include inputShadowActive;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({}),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
{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
|
||||
Add
|
||||
{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
|
||||
{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,
|
||||
};
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user