merges with master

This commit is contained in:
Jarrod Flesch
2020-07-29 13:19:47 -04:00
33 changed files with 529 additions and 195 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload",
"version": "0.0.30",
"version": "0.0.32",
"description": "CMS and Application Framework",
"license": "ISC",
"author": "Payload CMS LLC",

View File

@@ -27,7 +27,18 @@ async function register(args) {
}
// /////////////////////////////////////
// 2. Execute before create hook
// 2. Execute field-level hooks, access, and validation
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data,
hook: 'beforeCreate',
operationName: 'create',
req,
});
// /////////////////////////////////////
// 3. Execute before create hook
// /////////////////////////////////////
await collectionConfig.hooks.beforeCreate.reduce(async (priorHook, hook) => {
@@ -39,17 +50,6 @@ async function register(args) {
})) || 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
// /////////////////////////////////////

View File

@@ -54,7 +54,19 @@ async function update(args) {
let { data } = args;
// /////////////////////////////////////
// 2. Execute before update hook
// 2. Execute field-level hooks, access, and validation
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data,
req,
hook: 'beforeUpdate',
operationName: 'update',
originalDoc,
});
// /////////////////////////////////////
// 3. Execute before update hook
// /////////////////////////////////////
await collectionConfig.hooks.beforeUpdate.reduce(async (priorHook, hook) => {
@@ -68,23 +80,11 @@ async function update(args) {
}, Promise.resolve());
// /////////////////////////////////////
// 3. Merge updates into existing data
// 4. 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
// /////////////////////////////////////

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import AnimateHeight from 'react-animate-height';
import { Draggable } from 'react-beautiful-dnd';
@@ -34,6 +34,7 @@ const DraggableSection = (props) => {
actionPanelVerticalAlignment,
permissions,
isOpen,
readOnly,
} = props;
const [isHovered, setIsHovered] = useState(false);
@@ -48,6 +49,7 @@ const DraggableSection = (props) => {
<Draggable
draggableId={id}
index={rowIndex}
isDropDisabled={readOnly}
>
{(providedDrag) => (
<div
@@ -79,6 +81,7 @@ const DraggableSection = (props) => {
<SectionTitle
label={singularLabel}
path={`${parentPath}.${rowIndex}.blockName`}
readOnly={readOnly}
/>
<Button
@@ -96,6 +99,7 @@ const DraggableSection = (props) => {
duration={0}
>
<RenderFields
readOnly={readOnly}
customComponentsPath={customComponentsPath}
fieldTypes={fieldTypes}
key={rowIndex}
@@ -113,15 +117,17 @@ const DraggableSection = (props) => {
className="actions"
dragHandleProps={providedDrag.dragHandleProps}
>
<ActionPanel
rowIndex={rowIndex}
addRow={addRow}
removeRow={removeRow}
singularLabel={singularLabel}
verticalAlignment={actionPanelVerticalAlignment}
isHovered={isHovered}
{...props}
/>
{!readOnly && (
<ActionPanel
rowIndex={rowIndex}
addRow={addRow}
removeRow={removeRow}
singularLabel={singularLabel}
verticalAlignment={actionPanelVerticalAlignment}
isHovered={isHovered}
{...props}
/>
)}
</FieldTypeGutter>
</div>
</div>
@@ -141,6 +147,7 @@ DraggableSection.defaultProps = {
positionPanelVerticalAlignment: 'sticky',
actionPanelVerticalAlignment: 'sticky',
permissions: {},
readOnly: false,
};
DraggableSection.propTypes = {
@@ -162,6 +169,7 @@ DraggableSection.propTypes = {
positionPanelVerticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']),
actionPanelVerticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']),
permissions: PropTypes.shape({}),
readOnly: PropTypes.bool,
};
export default DraggableSection;

View File

@@ -80,6 +80,8 @@ const RenderFields = (props) => {
if (readOnlyOverride) readOnly = true;
if (operation === 'create') readOnly = false;
if (permissions?.[field?.name]?.read?.permission !== false) {
if (permissions?.[field?.name]?.[operation]?.permission === false) {
readOnly = true;

View File

@@ -29,6 +29,9 @@ const ArrayFieldType = (props) => {
minRows,
singularLabel,
permissions,
admin: {
readOnly,
},
} = props;
const [rows, dispatchRows] = useReducer(reducer, []);
@@ -113,6 +116,7 @@ const ArrayFieldType = (props) => {
fields={fields}
permissions={permissions}
value={value}
readOnly={readOnly}
/>
);
};
@@ -125,6 +129,7 @@ ArrayFieldType.defaultProps = {
minRows: undefined,
singularLabel: 'Row',
permissions: {},
admin: {},
};
ArrayFieldType.propTypes = {
@@ -143,6 +148,9 @@ ArrayFieldType.propTypes = {
permissions: PropTypes.shape({
fields: PropTypes.shape({}),
}),
admin: PropTypes.shape({
readOnly: PropTypes.bool,
}),
};
const RenderArray = React.memo((props) => {
@@ -163,6 +171,7 @@ const RenderArray = React.memo((props) => {
fields,
permissions,
value,
readOnly,
} = props;
return (
@@ -183,6 +192,7 @@ const RenderArray = React.memo((props) => {
>
{rows.length > 0 && rows.map((row, i) => (
<DraggableSection
readOnly={readOnly}
key={row.key}
id={row.key}
blockType="array"
@@ -205,18 +215,19 @@ const RenderArray = React.memo((props) => {
</div>
)}
</Droppable>
<div className={`${baseClass}__add-button-wrap`}>
<Button
onClick={() => addRow(value)}
buttonStyle="icon-label"
icon="plus"
iconStyle="with-border"
iconPosition="left"
>
{`Add ${singularLabel}`}
</Button>
</div>
{!readOnly && (
<div className={`${baseClass}__add-button-wrap`}>
<Button
onClick={() => addRow(value)}
buttonStyle="icon-label"
icon="plus"
iconStyle="with-border"
iconPosition="left"
>
{`Add ${singularLabel}`}
</Button>
</div>
)}
</div>
</DragDropContext>
);
@@ -231,6 +242,7 @@ RenderArray.defaultProps = {
path: '',
customComponentsPath: undefined,
value: undefined,
readOnly: false,
};
RenderArray.propTypes = {
@@ -256,6 +268,7 @@ RenderArray.propTypes = {
permissions: PropTypes.shape({
fields: PropTypes.shape({}),
}).isRequired,
readOnly: PropTypes.bool,
};
export default withCondition(ArrayFieldType);

View File

@@ -33,6 +33,9 @@ const Blocks = (props) => {
required,
validate,
permissions,
admin: {
readOnly,
},
} = props;
const path = pathFromProps || name;
@@ -130,6 +133,7 @@ const Blocks = (props) => {
permissions={permissions}
value={value}
blocks={blocks}
readOnly={readOnly}
/>
);
};
@@ -142,6 +146,7 @@ Blocks.defaultProps = {
maxRows: undefined,
minRows: undefined,
permissions: {},
admin: {},
};
Blocks.propTypes = {
@@ -160,6 +165,9 @@ Blocks.propTypes = {
permissions: PropTypes.shape({
fields: PropTypes.shape({}),
}),
admin: PropTypes.shape({
readOnly: PropTypes.bool,
}),
};
const RenderBlocks = React.memo((props) => {
@@ -181,6 +189,7 @@ const RenderBlocks = React.memo((props) => {
value,
toggleCollapse,
blocks,
readOnly,
} = props;
return (
@@ -195,7 +204,10 @@ const RenderBlocks = React.memo((props) => {
/>
</header>
<Droppable droppableId="blocks-drop">
<Droppable
droppableId="blocks-drop"
isDropDisabled={readOnly}
>
{(provided) => (
<div
ref={provided.innerRef}
@@ -208,6 +220,7 @@ const RenderBlocks = React.memo((props) => {
if (blockToRender) {
return (
<DraggableSection
readOnly={readOnly}
key={row.key}
id={row.key}
blockType="blocks"

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import useFieldType from '../../useFieldType';
import withCondition from '../../withCondition';
@@ -45,17 +45,21 @@ const Checkbox = (props) => {
disableFormData,
});
const classes = [
'field-type',
baseClass,
showError && 'error',
value && `${baseClass}--checked`,
readOnly && 'read-only',
].filter(Boolean).join(' ');
useEffect(() => {
if (value === null || value === undefined) {
setValue(false);
}
}, [value, setValue]);
return (
<div
className={classes}
className={[
'field-type',
baseClass,
showError && 'error',
value && `${baseClass}--checked`,
readOnly && `${baseClass}--read-only`,
].filter(Boolean).join(' ')}
style={{
...style,
width,
@@ -74,7 +78,7 @@ const Checkbox = (props) => {
/>
<button
type="button"
onClick={() => {
onClick={readOnly ? undefined : () => {
setValue(!value);
if (typeof onChange === 'function') onChange(!value);
}}

View File

@@ -55,4 +55,10 @@
}
}
}
&--read-only {
&__input {
background-color: lighten($color-gray, 5%);
}
}
}

View File

@@ -10,7 +10,14 @@ const baseClass = 'group';
const Group = (props) => {
const {
label, fields, name, path: pathFromProps, fieldTypes,
label,
fields,
name,
path: pathFromProps,
fieldTypes,
admin: {
readOnly,
},
} = props;
const path = pathFromProps || name;
@@ -26,6 +33,7 @@ const Group = (props) => {
<div className={`${baseClass}__fields-wrapper`}>
<RenderFields
readOnly={readOnly}
fieldTypes={fieldTypes}
customComponentsPath={`${customComponentsPath}${name}.fields.`}
fieldSchema={fields.map((subField) => ({
@@ -42,6 +50,7 @@ const Group = (props) => {
Group.defaultProps = {
label: '',
path: '',
admin: {},
};
Group.propTypes = {
@@ -52,6 +61,9 @@ Group.propTypes = {
name: PropTypes.string.isRequired,
path: PropTypes.string,
fieldTypes: PropTypes.shape({}).isRequired,
admin: PropTypes.shape({
readOnly: PropTypes.bool,
}),
};
export default withCondition(Group);

View File

@@ -59,3 +59,29 @@
}
}
}
.radio-group--read-only {
.radio-input {
&__label {
color: $color-gray;
}
&--is-selected {
.radio-input__styled-radio {
&:before {
background-color: $color-gray;
}
}
}
&:not(&--is-selected) {
&:hover {
.radio-input__styled-radio {
&:before {
opacity: 0;
}
}
}
}
}
}

View File

@@ -10,6 +10,8 @@ import { radio } from '../../../../../fields/validations';
import './index.scss';
const baseClass = 'radio-group';
const RadioGroup = (props) => {
const {
name,
@@ -19,6 +21,7 @@ const RadioGroup = (props) => {
label,
admin: {
readOnly,
layout = 'horizontal',
style,
width,
} = {},
@@ -45,9 +48,10 @@ const RadioGroup = (props) => {
const classes = [
'field-type',
'radio-group',
baseClass,
`${baseClass}--layout-${layout}`,
showError && 'error',
readOnly && 'read-only',
readOnly && `${baseClass}--read-only`,
].filter(Boolean).join(' ');
return (
@@ -67,18 +71,22 @@ const RadioGroup = (props) => {
label={label}
required={required}
/>
{options?.map((option) => {
const isSelected = option.value === value;
<ul className={`${baseClass}--group`}>
{options?.map((option) => {
const isSelected = option.value === value;
return (
<RadioInput
key={option.value}
isSelected={isSelected}
option={option}
onChange={readOnly ? undefined : setValue}
/>
);
})}
return (
<li key={option.value}>
<RadioInput
key={option.value}
isSelected={isSelected}
option={option}
onChange={readOnly ? undefined : setValue}
/>
</li>
);
})}
</ul>
</div>
);
};

View File

@@ -0,0 +1,21 @@
@import '../../../../scss/styles.scss';
.radio-group {
&--layout-horizontal {
ul {
display: flex;
flex-wrap: wrap;
}
li {
padding-right: $baseline;
flex-shrink: 0;
}
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
}

View File

@@ -25,15 +25,18 @@ class Relationship extends Component {
constructor(props) {
super(props);
const { relationTo, hasMultipleRelations } = this.props;
const { relationTo, hasMultipleRelations, required } = this.props;
const relations = hasMultipleRelations ? relationTo : [relationTo];
this.initialOptions = required ? [] : [{ value: 'null', label: 'None' }];
this.state = {
relations,
lastFullyLoadedRelation: -1,
lastLoadedPage: 1,
options: [],
errorLoading: false,
loadedIDs: [],
options: this.initialOptions,
};
}
@@ -41,11 +44,15 @@ class Relationship extends Component {
this.getNextOptions();
}
componentDidUpdate(prevProps, prevState) {
const { search } = this.state;
componentDidUpdate(_, prevState) {
const { search, options } = this.state;
if (search !== prevState.search) {
this.getNextOptions({ clear: true });
}
if (options !== prevState.options) {
this.ensureValueHasOption();
}
}
getNextOptions = (params = {}) => {
@@ -54,7 +61,9 @@ class Relationship extends Component {
if (clear) {
this.setState({
options: [],
options: this.initialOptions,
loadedIDs: [],
lastFullyLoadedRelation: -1,
});
}
@@ -63,7 +72,7 @@ class Relationship extends Component {
relations, lastFullyLoadedRelation, lastLoadedPage, search,
} = this.state;
const relationsToSearch = relations.slice(lastFullyLoadedRelation + 1);
const relationsToSearch = lastFullyLoadedRelation === -1 ? relations : relations.slice(lastFullyLoadedRelation + 1);
if (relationsToSearch.length > 0) {
some(relationsToSearch, async (relation, callback) => {
@@ -97,6 +106,9 @@ class Relationship extends Component {
if (nextPage) {
const { data, relation } = nextPage;
this.addOptions(data, relation);
this.setState({
lastLoadedPage: lastLoadedPage + 1,
});
} else {
const { data, relation } = lastPage;
this.addOptions(data, relation);
@@ -131,7 +143,7 @@ class Relationship extends Component {
if (hasMultipleRelations) {
options.forEach((option) => {
const potentialValue = option.options.find((subOption) => {
const potentialValue = option.options && option.options.find((subOption) => {
if (subOption?.value?.value && value?.value) {
return subOption.value.value === value.value;
}
@@ -154,61 +166,124 @@ class Relationship extends Component {
addOptions = (data, relation) => {
const { hasMultipleRelations } = this.props;
const { lastLoadedPage, options } = this.state;
const { options, loadedIDs } = this.state;
const collection = collections.find((coll) => coll.slug === relation);
if (!hasMultipleRelations) {
this.setState({
options: [
...options,
...data.docs.map((doc) => ({
label: doc[collection?.admin?.useAsTitle || 'id'],
value: doc.id,
})),
],
});
} else {
const allOptionGroups = [...options];
const optionsToAddTo = allOptionGroups.find((optionGroup) => optionGroup.label === collection.labels.plural);
const newlyLoadedIDs = [];
const newOptions = data.docs.map((doc) => ({
label: doc[collection?.admin?.useAsTitle || 'id'],
value: {
relationTo: collection.slug,
value: doc.id,
},
}));
let newOptions = [];
if (!hasMultipleRelations) {
newOptions = [
...options,
...data.docs.reduce((docs, doc) => {
if (loadedIDs.indexOf(doc.id) === -1) {
newlyLoadedIDs.push(doc.id);
return [
...docs,
{
label: doc[collection?.admin?.useAsTitle || 'id'],
value: doc.id,
},
];
}
return docs;
}, []),
];
} else {
newOptions = [...options];
const optionsToAddTo = newOptions.find((optionGroup) => optionGroup.label === collection.labels.plural);
const newSubOptions = data.docs.reduce((docs, doc) => {
if (loadedIDs.indexOf(doc.id) === -1) {
newlyLoadedIDs.push(doc.id);
return [
...docs,
{
label: doc[collection?.admin?.useAsTitle || 'id'],
value: {
relationTo: collection.slug,
value: doc.id,
},
},
];
}
return docs;
}, []);
if (optionsToAddTo) {
optionsToAddTo.options = [
...optionsToAddTo.options,
...newOptions,
...newSubOptions,
];
} else {
allOptionGroups.push({
newOptions.push({
label: collection.labels.plural,
options: newOptions,
options: newSubOptions,
});
}
this.setState({
options: [
...allOptionGroups,
],
});
}
this.setState({
lastLoadedPage: lastLoadedPage + 1,
options: newOptions,
loadedIDs: [
...loadedIDs,
...newlyLoadedIDs,
],
});
}
ensureValueHasOption = async () => {
const { relationTo, hasMany, value } = this.props;
const { options } = this.state;
const locatedValue = this.findValueInOptions(options, value);
const hasMultipleRelations = Array.isArray(relationTo);
if (!locatedValue && value) {
if (hasMany) {
value.forEach((val) => {
if (hasMultipleRelations) {
this.addOptionByID(val.value, val.relationTo);
} else {
this.addOptionByID(val, relationTo);
}
});
} else if (hasMultipleRelations) {
this.addOptionByID(value.value, value.relationTo);
} else {
this.addOptionByID(value, relationTo);
}
}
}
addOptionByID = async (id, relation) => {
const { errorLoading } = this.state;
if (!errorLoading) {
const response = await fetch(`${serverURL}${api}/${relation}/${id}`);
if (response.ok) {
const data = await response.json();
this.addOptions({ docs: [data] }, relation);
} else {
console.log(`There was a problem loading the document with ID of ${id}.`);
}
}
}
handleInputChange = (search) => {
this.setState({
search,
lastFullyLoadedRelation: -1,
lastLoadedPage: 1,
});
const { search: existingSearch } = this.state;
if (search !== existingSearch) {
this.setState({
search,
lastFullyLoadedRelation: -1,
lastLoadedPage: 1,
});
}
}
handleMenuScrollToBottom = () => {
@@ -240,10 +315,10 @@ class Relationship extends Component {
baseClass,
showError && 'error',
errorLoading && 'error-loading',
readOnly && 'read-only',
readOnly && `${baseClass}--read-only`,
].filter(Boolean).join(' ');
const valueToRender = this.findValueInOptions(options, value);
const valueToRender = this.findValueInOptions(options, value) || value;
return (
<div
@@ -264,6 +339,7 @@ class Relationship extends Component {
/>
{!errorLoading && (
<ReactSelect
isDisabled={readOnly}
onInputChange={this.handleInputChange}
onChange={!readOnly ? setValue : undefined}
formatValue={this.formatSelectedValue}

View File

@@ -11,3 +11,11 @@
background-color: $color-red;
color: white;
}
.relationship--read-only {
div.react-select {
div.rs__control {
background: lighten($color-light-gray, 5%);
}
}
}

View File

@@ -7,11 +7,18 @@ import './index.scss';
const Row = (props) => {
const {
fields, fieldTypes, path, permissions,
fields,
fieldTypes,
path,
permissions,
admin: {
readOnly,
},
} = props;
return (
<RenderFields
readOnly={readOnly}
className="field-type row"
permissions={permissions}
fieldTypes={fieldTypes}
@@ -26,6 +33,7 @@ const Row = (props) => {
Row.defaultProps = {
path: '',
permissions: {},
admin: {},
};
Row.propTypes = {
@@ -35,6 +43,9 @@ Row.propTypes = {
fieldTypes: PropTypes.shape({}).isRequired,
path: PropTypes.string,
permissions: PropTypes.shape({}),
admin: PropTypes.shape({
readOnly: PropTypes.bool,
}),
};
export default withCondition(Row);

View File

@@ -9,6 +9,8 @@ import { select } from '../../../../../fields/validations';
import './index.scss';
const baseClass = 'select';
const findFullOption = (value, options) => {
const matchedOption = options.find((option) => option?.value === value);
@@ -97,9 +99,9 @@ const Select = (props) => {
const classes = [
'field-type',
'select',
baseClass,
showError && 'error',
readOnly && 'read-only',
readOnly && `${baseClass}--read-only`,
].filter(Boolean).join(' ');
const valueToRender = formatRenderValue(value, options);
@@ -126,7 +128,7 @@ const Select = (props) => {
value={valueToRender}
formatValue={formatFormValue}
showError={showError}
disabled={readOnly}
isDisabled={readOnly}
options={options}
isMulti={hasMany}
/>

View File

@@ -3,3 +3,11 @@
.field-type.select {
position: relative;
}
.select--read-only {
div.react-select {
div.rs__control {
background: lighten($color-light-gray, 5%);
}
}
}

View File

@@ -37,8 +37,8 @@
}
&:disabled {
background: $color-light-gray;
color: $color-gray;
background: lighten($color-light-gray, 5%);
color: darken($color-gray, 5%);
&:hover {
border-color: $color-light-gray;

View File

@@ -46,7 +46,7 @@ const CreateFirstUser = (props) => {
return (
<MinimalTemplate className={baseClass}>
<h1>Welcome to Payload</h1>
<h1>Welcome</h1>
<p>To begin, create your first user.</p>
<Form
onSuccess={onSuccess}

View File

@@ -45,6 +45,7 @@ const DefaultEditView = (props) => {
fields,
admin: {
useAsTitle,
disableDuplicate,
},
timestamps,
preview,
@@ -110,10 +111,12 @@ const DefaultEditView = (props) => {
<div className={`${baseClass}__sidebar`}>
{isEditing ? (
<ul className={`${baseClass}__collection-actions`}>
{permissions?.create?.permission && (
{(permissions?.create?.permission) && (
<React.Fragment>
<li><Link to={`${admin}/collections/${slug}/create`}>Create New</Link></li>
<li><DuplicateDocument slug={slug} /></li>
{!disableDuplicate && (
<li><DuplicateDocument slug={slug} /></li>
)}
</React.Fragment>
)}
{permissions?.delete?.permission && (
@@ -220,6 +223,7 @@ DefaultEditView.propTypes = {
slug: PropTypes.string,
admin: PropTypes.shape({
useAsTitle: PropTypes.string,
disableDuplicate: PropTypes.bool,
}),
fields: PropTypes.arrayOf(PropTypes.shape({})),
preview: PropTypes.func,

View File

@@ -0,0 +1,75 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import config from 'payload/config';
const { collections } = config;
const RelationshipCell = (props) => {
const { field, cellData } = props;
const { relationTo } = field;
const [data, setData] = useState();
console.log(cellData);
useEffect(() => {
const hasManyRelations = Array.isArray(relationTo);
if (cellData) {
if (Array.isArray(cellData)) {
setData(cellData.reduce((newData, value) => {
const relation = hasManyRelations ? value?.relationTo : relationTo;
const doc = hasManyRelations ? value.value : value;
const collection = collections.find((coll) => coll.slug === relation);
if (collection) {
const useAsTitle = collection.admin.useAsTitle ? collection.admin.useAsTitle : 'id';
return newData ? `${newData}, ${doc[useAsTitle]}` : doc[useAsTitle];
}
return newData;
}, ''));
} else {
const relation = hasManyRelations ? cellData?.relationTo : relationTo;
const doc = hasManyRelations ? cellData.value : cellData;
const collection = collections.find((coll) => coll.slug === relation);
if (collection) {
const useAsTitle = collection.admin.useAsTitle ? collection.admin.useAsTitle : 'id';
setData(doc[useAsTitle]);
}
}
}
}, [cellData, relationTo, field]);
return (
<React.Fragment>
{data}
</React.Fragment>
);
};
RelationshipCell.defaultProps = {
cellData: undefined,
};
RelationshipCell.propTypes = {
cellData: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.array,
PropTypes.string,
]),
field: PropTypes.shape({
relationTo: PropTypes.oneOfType([
PropTypes.arrayOf(
PropTypes.string,
),
PropTypes.string,
]),
}).isRequired,
};
export default RelationshipCell;

View File

@@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import format from 'date-fns/format';
import config from 'payload/config';
import RenderCustomComponent from '../../../utilities/RenderCustomComponent';
import Thumbnail from '../../../elements/Thumbnail';
import RenderCustomComponent from '../../../../utilities/RenderCustomComponent';
import Thumbnail from '../../../../elements/Thumbnail';
import Relationship from './Relationship';
const { routes: { admin } } = config;
@@ -50,6 +51,17 @@ const DefaultCell = (props) => {
);
}
if (field.type === 'relationship') {
return (
<WrapElement {...wrapElementProps}>
<Relationship
field={field}
cellData={cellData}
/>
</WrapElement>
);
}
if (field.type === 'date' && cellData) {
return (
<WrapElement {...wrapElementProps}>
@@ -65,6 +77,7 @@ const DefaultCell = (props) => {
{field.type !== 'date' && (
<React.Fragment>
{typeof cellData === 'string' && cellData}
{typeof cellData === 'number' && cellData}
{typeof cellData === 'object' && JSON.stringify(cellData)}
</React.Fragment>
)}

View File

@@ -48,7 +48,7 @@ const DefaultList = (props) => {
useEffect(() => {
const params = {
depth: 0,
depth: 2,
};
if (page) params.page = page;

View File

@@ -4,7 +4,6 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Payload</title>
</head>
<body>

View File

@@ -33,7 +33,18 @@ async function create(args) {
await executeAccess({ req }, collectionConfig.access.create);
// /////////////////////////////////////
// 2. Execute before collection hook
// 2. Execute field-level access, hooks, and validation
// /////////////////////////////////////
data = await performFieldOperations(collectionConfig, {
data,
hook: 'beforeCreate',
operationName: 'create',
req,
});
// /////////////////////////////////////
// 3. Execute before collection hook
// /////////////////////////////////////
await collectionConfig.hooks.beforeCreate.reduce(async (priorHook, hook) => {
@@ -46,37 +57,39 @@ async function create(args) {
}, Promise.resolve());
// /////////////////////////////////////
// 3. Upload and resize any files that may be present
// 4. Upload and resize any files that may be present
// /////////////////////////////////////
if (collectionConfig.upload) {
const { staticDir, imageSizes } = collectionConfig.upload;
const fileData = {};
if (!req.files || Object.keys(req.files).length === 0) {
const { staticDir, imageSizes } = collectionConfig.upload;
const file = (req.files && req.files.file) ? req.files.file : req.file;
if (!file) {
throw new MissingFile();
}
mkdirp.sync(staticDir);
const fsSafeName = await getSafeFilename(staticDir, req.files.file.name);
const fsSafeName = await getSafeFilename(staticDir, file.name);
await req.files.file.mv(`${staticDir}/${fsSafeName}`);
await file.mv(`${staticDir}/${fsSafeName}`);
if (imageMIMETypes.indexOf(req.files.file.mimetype) > -1) {
if (imageMIMETypes.indexOf(file.mimetype) > -1) {
const dimensions = await getImageSize(`${staticDir}/${fsSafeName}`);
fileData.width = dimensions.width;
fileData.height = dimensions.height;
if (Array.isArray(imageSizes) && req.files.file.mimetype !== 'image/svg+xml') {
if (Array.isArray(imageSizes) && file.mimetype !== 'image/svg+xml') {
fileData.sizes = await resizeAndSave(collectionConfig, fsSafeName, fileData.mimeType);
}
}
fileData.filename = fsSafeName;
fileData.filesize = req.files.file.size;
fileData.mimeType = req.files.file.mimetype;
fileData.filesize = file.size;
fileData.mimeType = file.mimetype;
data = {
...data,
@@ -84,17 +97,6 @@ async function create(args) {
};
}
// /////////////////////////////////////
// 4. Execute field-level access, hooks, and validation
// /////////////////////////////////////
data = await performFieldOperations(collectionConfig, {
data,
hook: 'beforeCreate',
operationName: 'create',
req,
});
// /////////////////////////////////////
// 5. Perform database operation
// /////////////////////////////////////

View File

@@ -22,6 +22,7 @@ async function find(options) {
payloadAPI: 'local',
locale,
fallbackLocale,
payload: this,
},
});
}

View File

@@ -64,11 +64,23 @@ async function update(args) {
const originalDoc = doc.toJSON({ virtuals: true });
// /////////////////////////////////////
// 2. Execute before update hook
// 2. Execute field-level hooks, access, and validation
// /////////////////////////////////////
let { data } = args;
data = await this.performFieldOperations(collectionConfig, {
data,
req,
originalDoc,
hook: 'beforeUpdate',
operationName: 'update',
});
// /////////////////////////////////////
// 3. Execute before update hook
// /////////////////////////////////////
await collectionConfig.hooks.beforeUpdate.reduce(async (priorHook, hook) => {
await priorHook;
@@ -80,23 +92,11 @@ async function update(args) {
}, Promise.resolve());
// /////////////////////////////////////
// 3. Merge updates into existing data
// 4. 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,
originalDoc,
hook: 'beforeUpdate',
operationName: 'update',
});
// /////////////////////////////////////
// 5. Upload and resize any files that may be present
// /////////////////////////////////////
@@ -106,21 +106,23 @@ async function update(args) {
const { staticDir, imageSizes } = collectionConfig.upload;
if (req.files && req.files.file) {
const fsSafeName = await getSafeFilename(staticDir, req.files.file.name);
const file = (req.files && req.files.file) ? req.files.file : req.fileData;
await req.files.file.mv(`${staticDir}/${fsSafeName}`);
if (file) {
const fsSafeName = await getSafeFilename(staticDir, file.name);
await file.mv(`${staticDir}/${fsSafeName}`);
fileData.filename = fsSafeName;
fileData.filesize = req.files.file.size;
fileData.mimeType = req.files.file.mimetype;
fileData.filesize = file.size;
fileData.mimeType = file.mimetype;
if (imageMIMETypes.indexOf(req.files.file.mimetype) > -1) {
if (imageMIMETypes.indexOf(file.mimetype) > -1) {
const dimensions = await getImageSize(`${staticDir}/${fsSafeName}`);
fileData.width = dimensions.width;
fileData.height = dimensions.height;
if (Array.isArray(imageSizes) && req.files.file.mimetype !== 'image/svg+xml') {
if (Array.isArray(imageSizes) && file.mimetype !== 'image/svg+xml') {
fileData.sizes = await resizeAndSave(collectionConfig, fsSafeName, fileData.mimeType);
}
}

View File

@@ -102,6 +102,14 @@ const sanitizeCollection = (collections, collection) => {
{
name: 'filename',
label: 'Filename',
hooks: {
beforeCreate: [
({ req }) => {
const file = (req.files && req.files.file) ? req.files.file : req.file;
return file.name;
},
],
},
type: 'text',
required: true,
unique: true,

View File

@@ -2,7 +2,7 @@ const httpStatus = require('http-status');
const formatErrorResponse = require('../responses/formatError');
const logger = require('../../utilities/logger')();
const errorHandler = (config) => async (err, req, res) => {
const errorHandler = (config) => async (err, req, res, next) => {
const data = formatErrorResponse(err);
let response;
let status = err.status || httpStatus.INTERNAL_SERVER_ERROR;

View File

@@ -24,17 +24,18 @@ const middleware = (payload) => [
}),
(req, _, next) => {
req.payload = payload;
return next();
next();
},
(req, res, next) => {
if (payload.config.cors) {
if (payload.config.cors.indexOf(req.headers.origin) > -1) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
}
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin X-Requested-With, Content-Type, Accept, Authorization');
res.header('Access-Control-Allow-Headers',
'Origin X-Requested-With, Content-Type, Accept, Authorization');
if (payload.config.cors === '*') {
res.setHeader('Access-Control-Allow-Origin', '*');
} else if (Array.isArray(payload.config.cors) && payload.config.cors.indexOf(req.headers.origin) > -1) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
}
}
next();

View File

@@ -73,12 +73,12 @@ async function performFieldOperations(entityConfig, operation) {
const hookPromises = [];
const errors = [];
const createValidationPromise = async (newValue, existingValue, field, path) => {
const createValidationPromise = async (newData, existingData, field, path) => {
const hasCondition = field.admin && field.admin.condition;
const shouldValidate = field.validate && !hasCondition;
let valueToValidate = newValue;
if (valueToValidate === undefined) valueToValidate = existingValue;
let valueToValidate = newData[field.name];
if (valueToValidate === undefined) valueToValidate = existingData[field.name];
if (valueToValidate === undefined) valueToValidate = field.defaultValue;
const result = shouldValidate ? await field.validate(valueToValidate, field) : true;
@@ -131,9 +131,13 @@ async function performFieldOperations(entityConfig, operation) {
}
};
const createHookPromise = async (data, field) => {
const createHookPromise = async (data, originalDoc, field) => {
const resultingData = data;
if ((field.type === 'relationship' || field.type === 'upload') && (data[field.name] === 'null' || data[field.name] === null)) {
resultingData[field.name] = null;
}
if (hook === 'afterRead') {
if ((field.type === 'relationship' || field.type === 'upload')) {
const hasManyRelations = Array.isArray(field.relationTo);
@@ -235,14 +239,16 @@ async function performFieldOperations(entityConfig, operation) {
}
if (field.hooks && field.hooks[hook]) {
field.hooks[hook].forEach(async (fieldHook) => {
resultingData[field.name] = await fieldHook({
await field.hooks[hook].reduce(async (priorHook, currentHook) => {
await priorHook;
resultingData[field.name] = await currentHook({
value: data[field.name],
originalDoc: fullOriginalDoc,
data: fullData,
req,
});
});
}) || resultingData[field.name];
}, Promise.resolve());
}
};
@@ -262,7 +268,7 @@ async function performFieldOperations(entityConfig, operation) {
}
accessPromises.push(createAccessPromise(data, originalDoc, field));
hookPromises.push(createHookPromise(data, field));
hookPromises.push(createHookPromise(data, originalDoc, field));
if (field.fields) {
if (field.name === undefined) {
@@ -293,9 +299,9 @@ async function performFieldOperations(entityConfig, operation) {
const hasRowsOfExistingData = Array.isArray(originalDoc[field.name]);
const existingRowCount = hasRowsOfExistingData ? originalDoc[field.name].length : 0;
validationPromises.push(createValidationPromise(newRowCount, existingRowCount, field, path));
validationPromises.push(() => createValidationPromise({ [field.name]: newRowCount }, { [field.name]: existingRowCount }, field, path));
} else if (field.name) {
validationPromises.push(createValidationPromise(data[field.name], originalDoc[field.name], field, path));
validationPromises.push(() => createValidationPromise(data, originalDoc, field, path, true));
}
}
});
@@ -306,6 +312,11 @@ async function performFieldOperations(entityConfig, operation) {
// //////////////////////////////////////////
traverseFields(entityConfig.fields, fullData, fullOriginalDoc, '');
await Promise.all(hookPromises);
validationPromises.forEach((promise) => promise());
await Promise.all(validationPromises);
if (errors.length > 0) {
@@ -314,7 +325,6 @@ async function performFieldOperations(entityConfig, operation) {
await Promise.all(accessPromises);
await Promise.all(relationshipPopulationPromises);
await Promise.all(hookPromises);
return fullData;
}