Merge branch 'feat/1.0' of github.com:payloadcms/payload into feat/1.0
This commit is contained in:
@@ -25,7 +25,9 @@ const ButtonContents = ({ children, icon, tooltip }) => {
|
||||
const BuiltInIcon = icons[icon];
|
||||
|
||||
return (
|
||||
<span className={`${baseClass}__content`}>
|
||||
<span
|
||||
className={`${baseClass}__content`}
|
||||
>
|
||||
{tooltip && (
|
||||
<Tooltip className={`${baseClass}__tooltip`}>
|
||||
{tooltip}
|
||||
@@ -49,6 +51,7 @@ const ButtonContents = ({ children, icon, tooltip }) => {
|
||||
const Button: React.FC<Props> = (props) => {
|
||||
const {
|
||||
className,
|
||||
id,
|
||||
type = 'button',
|
||||
el,
|
||||
to,
|
||||
@@ -86,6 +89,7 @@ const Button: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
const buttonProps = {
|
||||
id,
|
||||
type,
|
||||
className: classes,
|
||||
disabled,
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { MouseEvent } from 'react';
|
||||
|
||||
export type Props = {
|
||||
className?: string,
|
||||
id?: string,
|
||||
type?: 'submit' | 'button',
|
||||
el?: 'link' | 'anchor' | undefined,
|
||||
to?: string,
|
||||
@@ -12,6 +13,7 @@ export type Props = {
|
||||
icon?: React.ReactNode | ['chevron' | 'x' | 'plus' | 'edit'],
|
||||
iconStyle?: 'with-border' | 'without-border' | 'none',
|
||||
buttonStyle?: 'primary' | 'secondary' | 'transparent' | 'error' | 'none' | 'icon-label',
|
||||
buttonId?: string,
|
||||
round?: boolean,
|
||||
size?: 'small' | 'medium',
|
||||
iconPosition?: 'left' | 'right',
|
||||
|
||||
@@ -7,15 +7,19 @@ import './index.scss';
|
||||
const baseClass = 'card';
|
||||
|
||||
const Card: React.FC<Props> = (props) => {
|
||||
const { title, actions, onClick } = props;
|
||||
const { id, title, actions, onClick } = props;
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
id,
|
||||
onClick && `${baseClass}--has-onclick`,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div
|
||||
className={classes}
|
||||
id={id}
|
||||
>
|
||||
<h5>
|
||||
{title}
|
||||
</h5>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type Props = {
|
||||
id?: string,
|
||||
title: string,
|
||||
actions?: React.ReactNode,
|
||||
onClick?: () => void,
|
||||
|
||||
@@ -18,6 +18,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
||||
const {
|
||||
title: titleFromProps,
|
||||
id,
|
||||
buttonId,
|
||||
collection: {
|
||||
admin: {
|
||||
useAsTitle,
|
||||
@@ -78,6 +79,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
||||
<React.Fragment>
|
||||
<button
|
||||
type="button"
|
||||
id={buttonId}
|
||||
className={`${baseClass}__toggle`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -105,6 +107,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
||||
". Are you sure?
|
||||
</p>
|
||||
<Button
|
||||
id="confirm-cancel"
|
||||
buttonStyle="secondary"
|
||||
type="button"
|
||||
onClick={deleting ? undefined : () => toggle(modalSlug)}
|
||||
@@ -113,6 +116,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={deleting ? undefined : handleDelete}
|
||||
id="confirm-delete"
|
||||
>
|
||||
{deleting ? 'Deleting...' : 'Confirm'}
|
||||
</Button>
|
||||
|
||||
@@ -3,5 +3,6 @@ import { SanitizedCollectionConfig } from '../../../../collections/config/types'
|
||||
export type Props = {
|
||||
collection?: SanitizedCollectionConfig,
|
||||
id?: string,
|
||||
buttonId?: string,
|
||||
title?: string,
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const Duplicate: React.FC<Props> = ({ slug }) => {
|
||||
|
||||
return (
|
||||
<Button
|
||||
id="action-duplicate"
|
||||
buttonStyle="none"
|
||||
className={baseClass}
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -16,7 +16,10 @@ const Table: React.FC<Props> = ({ columns, data }) => {
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((col, i) => (
|
||||
<th key={i}>
|
||||
<th
|
||||
key={i}
|
||||
id={`heading-${col.accessor}`}
|
||||
>
|
||||
{col.components.Heading}
|
||||
</th>
|
||||
))}
|
||||
@@ -24,9 +27,15 @@ const Table: React.FC<Props> = ({ columns, data }) => {
|
||||
</thead>
|
||||
<tbody>
|
||||
{data && data.map((row, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
<tr
|
||||
key={rowIndex}
|
||||
className={`row-${rowIndex + 1}`}
|
||||
>
|
||||
{columns.map((col, colIndex) => (
|
||||
<td key={colIndex}>
|
||||
<td
|
||||
key={colIndex}
|
||||
className={`cell-${col.accessor}`}
|
||||
>
|
||||
{col.components.renderCell(row, row[col.accessor])}
|
||||
</td>
|
||||
))}
|
||||
|
||||
@@ -105,6 +105,7 @@ const Condition: React.FC<Props> = (props) => {
|
||||
<div className={`${baseClass}__actions`}>
|
||||
<Button
|
||||
icon="x"
|
||||
className={`${baseClass}__actions-remove`}
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
iconStyle="with-border"
|
||||
@@ -116,6 +117,7 @@ const Condition: React.FC<Props> = (props) => {
|
||||
/>
|
||||
<Button
|
||||
icon="plus"
|
||||
className={`${baseClass}__actions-add`}
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
iconStyle="with-border"
|
||||
|
||||
@@ -8,7 +8,7 @@ import './index.scss';
|
||||
const baseClass = 'form-submit';
|
||||
|
||||
const FormSubmit: React.FC<Props> = (props) => {
|
||||
const { children, disabled: disabledFromProps, type = 'submit' } = props;
|
||||
const { children, buttonId: id, disabled: disabledFromProps, type = 'submit' } = props;
|
||||
const processing = useFormProcessing();
|
||||
const { disabled } = useForm();
|
||||
|
||||
@@ -16,6 +16,7 @@ const FormSubmit: React.FC<Props> = (props) => {
|
||||
<div className={baseClass}>
|
||||
<Button
|
||||
{...props}
|
||||
id={id}
|
||||
type={type}
|
||||
disabled={disabledFromProps || processing || disabled ? true : undefined}
|
||||
>
|
||||
|
||||
@@ -202,6 +202,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div
|
||||
id={`field-${path}`}
|
||||
className={classes}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
|
||||
@@ -212,6 +212,7 @@ const Blocks: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div
|
||||
id={`field-${path}`}
|
||||
className={classes}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
|
||||
@@ -70,9 +70,9 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
id={`field-${path}`}
|
||||
type="checkbox"
|
||||
name={path}
|
||||
id={path}
|
||||
checked={Boolean(value)}
|
||||
readOnly
|
||||
/>
|
||||
|
||||
@@ -78,11 +78,12 @@ const Code: React.FC<Props> = (props) => {
|
||||
message={errorMessage}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={path}
|
||||
htmlFor={`field-${path}`}
|
||||
label={label}
|
||||
required={required}
|
||||
/>
|
||||
<Editor
|
||||
id={`field-${path}`}
|
||||
value={value as string || ''}
|
||||
onValueChange={readOnly ? () => null : setValue}
|
||||
highlight={highlighter}
|
||||
|
||||
@@ -43,7 +43,7 @@ const ConfirmPassword: React.FC = () => {
|
||||
message={errorMessage}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="confirm-password"
|
||||
htmlFor="field-confirm-password"
|
||||
label="Confirm Password"
|
||||
required
|
||||
/>
|
||||
@@ -52,7 +52,7 @@ const ConfirmPassword: React.FC = () => {
|
||||
onChange={setValue}
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
id="confirm-password"
|
||||
id="field-confirm-password"
|
||||
name="confirm-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,10 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
label={label}
|
||||
required={required}
|
||||
/>
|
||||
<div className={`${baseClass}__input-wrapper`}>
|
||||
<div
|
||||
className={`${baseClass}__input-wrapper`}
|
||||
id={`field-${path}`}
|
||||
>
|
||||
<DatePicker
|
||||
{...date}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -74,12 +74,12 @@ const Email: React.FC<Props> = (props) => {
|
||||
required={required}
|
||||
/>
|
||||
<input
|
||||
id={`field-${path}`}
|
||||
value={value as string || ''}
|
||||
onChange={setValue}
|
||||
disabled={Boolean(readOnly)}
|
||||
placeholder={placeholder}
|
||||
type="email"
|
||||
id={path}
|
||||
name={path}
|
||||
autoComplete={autoComplete}
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,7 @@ const Group: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`field-${path}`}
|
||||
className={[
|
||||
'field-type',
|
||||
baseClass,
|
||||
|
||||
@@ -25,6 +25,7 @@ const HiddenInput: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<input
|
||||
id={`field-${path}`}
|
||||
type="hidden"
|
||||
value={value as string || ''}
|
||||
onChange={setValue}
|
||||
|
||||
@@ -79,17 +79,17 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
message={errorMessage}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={path}
|
||||
htmlFor={`field-${path}`}
|
||||
label={label}
|
||||
required={required}
|
||||
/>
|
||||
<input
|
||||
id={`field-${path}`}
|
||||
value={typeof value === 'number' ? value : ''}
|
||||
onChange={handleChange}
|
||||
disabled={readOnly}
|
||||
placeholder={placeholder}
|
||||
type="number"
|
||||
id={path}
|
||||
name={path}
|
||||
step={step}
|
||||
/>
|
||||
|
||||
@@ -60,17 +60,17 @@ const Password: React.FC<Props> = (props) => {
|
||||
message={errorMessage}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={path}
|
||||
htmlFor={`field-${path}`}
|
||||
label={label}
|
||||
required={required}
|
||||
/>
|
||||
<input
|
||||
id={`field-${path}`}
|
||||
value={value as string || ''}
|
||||
onChange={setValue}
|
||||
disabled={formProcessing}
|
||||
type="password"
|
||||
autoComplete={autoComplete}
|
||||
id={path}
|
||||
name={path}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -81,34 +81,34 @@ const PointField: React.FC<Props> = (props) => {
|
||||
<ul className={`${baseClass}__wrap`}>
|
||||
<li>
|
||||
<Label
|
||||
htmlFor={`${path}.longitude`}
|
||||
htmlFor={`field-longitude-${path}`}
|
||||
label={`${label} - Longitude`}
|
||||
required={required}
|
||||
/>
|
||||
<input
|
||||
id={`field-longitude-${path}`}
|
||||
value={(value && typeof value[0] === 'number') ? value[0] : ''}
|
||||
onChange={(e) => handleChange(e, 0)}
|
||||
disabled={readOnly}
|
||||
placeholder={placeholder}
|
||||
type="number"
|
||||
id={`${path}.longitude`}
|
||||
name={`${path}.longitude`}
|
||||
step={step}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Label
|
||||
htmlFor={`${path}.latitude`}
|
||||
htmlFor={`field-latitude-${path}`}
|
||||
label={`${label} - Latitude`}
|
||||
required={required}
|
||||
/>
|
||||
<input
|
||||
id={`field-latitude-${path}`}
|
||||
value={(value && typeof value[1] === 'number') ? value[1] : ''}
|
||||
onChange={(e) => handleChange(e, 1)}
|
||||
disabled={readOnly}
|
||||
placeholder={placeholder}
|
||||
type="number"
|
||||
id={`${path}.latitude`}
|
||||
name={`${path}.latitude`}
|
||||
step={step}
|
||||
/>
|
||||
|
||||
@@ -73,11 +73,14 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
|
||||
/>
|
||||
</div>
|
||||
<Label
|
||||
htmlFor={path}
|
||||
htmlFor={`field-${path}`}
|
||||
label={label}
|
||||
required={required}
|
||||
/>
|
||||
<ul className={`${baseClass}--group`}>
|
||||
<ul
|
||||
id={`field-${path}`}
|
||||
className={`${baseClass}--group`}
|
||||
>
|
||||
{options.map((option) => {
|
||||
let optionValue = '';
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const RadioInput: React.FC<Props> = (props) => {
|
||||
isSelected && `${baseClass}--is-selected`,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const id = `${path}-${option.value}`;
|
||||
const id = `field-${path}-${option.value}`;
|
||||
|
||||
return (
|
||||
<label
|
||||
|
||||
@@ -335,6 +335,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`field-${path}`}
|
||||
className={classes}
|
||||
style={{
|
||||
...style,
|
||||
|
||||
@@ -209,7 +209,7 @@ const RichText: React.FC<Props> = (props) => {
|
||||
message={errorMessage}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={path}
|
||||
htmlFor={`field-${path}`}
|
||||
label={label}
|
||||
required={required}
|
||||
/>
|
||||
@@ -270,6 +270,7 @@ const RichText: React.FC<Props> = (props) => {
|
||||
ref={editorRef}
|
||||
>
|
||||
<Editable
|
||||
id={`field-${path}`}
|
||||
className={`${baseClass}__input`}
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
|
||||
@@ -62,6 +62,7 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`field-${path}`}
|
||||
className={classes}
|
||||
style={{
|
||||
...style,
|
||||
|
||||
@@ -61,17 +61,17 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
message={errorMessage}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={path}
|
||||
htmlFor={`field-${path}`}
|
||||
label={label}
|
||||
required={required}
|
||||
/>
|
||||
<input
|
||||
id={`field-${path}`}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
disabled={readOnly}
|
||||
placeholder={placeholder}
|
||||
type="text"
|
||||
id={path}
|
||||
name={path}
|
||||
/>
|
||||
<FieldDescription
|
||||
|
||||
@@ -62,16 +62,16 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
message={errorMessage}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={path}
|
||||
htmlFor={`field-${path}`}
|
||||
label={label}
|
||||
required={required}
|
||||
/>
|
||||
<textarea
|
||||
id={`field-${path}`}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
disabled={readOnly}
|
||||
placeholder={placeholder}
|
||||
id={path}
|
||||
name={path}
|
||||
rows={rows}
|
||||
/>
|
||||
|
||||
@@ -47,6 +47,7 @@ const Dashboard: React.FC<Props> = (props) => {
|
||||
<li key={collection.slug}>
|
||||
<Card
|
||||
title={collection.labels.plural}
|
||||
id={`card-${collection.slug}`}
|
||||
onClick={() => push({ pathname: `${admin}/collections/${collection.slug}` })}
|
||||
actions={hasCreatePermission ? (
|
||||
<Button
|
||||
|
||||
@@ -134,7 +134,14 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
<ul className={`${baseClass}__collection-actions`}>
|
||||
{(permissions?.create?.permission) && (
|
||||
<React.Fragment>
|
||||
<li><Link to={`${admin}/collections/${slug}/create`}>Create New</Link></li>
|
||||
<li>
|
||||
<Link
|
||||
id="action-create"
|
||||
to={`${admin}/collections/${slug}/create`}
|
||||
>
|
||||
Create New
|
||||
</Link>
|
||||
</li>
|
||||
{!disableDuplicate && (
|
||||
<li><DuplicateDocument slug={slug} /></li>
|
||||
)}
|
||||
@@ -145,6 +152,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
<DeleteDocument
|
||||
collection={collection}
|
||||
id={id}
|
||||
buttonId="action-delete"
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
@@ -167,7 +175,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
</React.Fragment>
|
||||
)}
|
||||
{!collection.versions?.drafts && (
|
||||
<FormSubmit>Save</FormSubmit>
|
||||
<FormSubmit buttonId="action-save">Save</FormSubmit>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.relationship-cell {
|
||||
min-width: 250px;
|
||||
}
|
||||
@@ -1,55 +1,68 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useConfig } from '../../../../../../utilities/Config';
|
||||
import useIntersect from '../../../../../../../hooks/useIntersect';
|
||||
import { useListRelationships } from '../../../RelationshipProvider';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
type Value = { relationTo: string, value: number | string };
|
||||
const baseClass = 'relationship-cell';
|
||||
const totalToShow = 3;
|
||||
|
||||
const RelationshipCell = (props) => {
|
||||
const { field, data: cellData } = props;
|
||||
const { relationTo } = field;
|
||||
const { collections } = useConfig();
|
||||
const [data, setData] = useState();
|
||||
const { collections, routes } = useConfig();
|
||||
const [intersectionRef, entry] = useIntersect();
|
||||
const [values, setValues] = useState<Value[]>([]);
|
||||
const { getRelationships, documents } = useListRelationships();
|
||||
const [hasRequested, setHasRequested] = useState(false);
|
||||
|
||||
const isAboveViewport = entry?.boundingClientRect?.top > 0;
|
||||
|
||||
useEffect(() => {
|
||||
const hasManyRelations = Array.isArray(relationTo);
|
||||
if (cellData && isAboveViewport && !hasRequested) {
|
||||
const formattedValues: Value[] = [];
|
||||
|
||||
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';
|
||||
let title: string;
|
||||
if (typeof doc === 'string') {
|
||||
title = doc;
|
||||
} else {
|
||||
title = doc?.[useAsTitle] ? doc[useAsTitle] : doc;
|
||||
}
|
||||
|
||||
return newData ? `${newData}, ${title}` : title;
|
||||
}
|
||||
|
||||
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 && doc) {
|
||||
const useAsTitle = collection.admin.useAsTitle ? collection.admin.useAsTitle : 'id';
|
||||
|
||||
setData(doc[useAsTitle] ? doc[useAsTitle] : doc);
|
||||
const arrayCellData = Array.isArray(cellData) ? cellData : [cellData];
|
||||
arrayCellData.slice(0, (arrayCellData.length < totalToShow ? arrayCellData.length : totalToShow)).forEach((cell) => {
|
||||
if (typeof cell === 'object' && 'relationTo' in cell && 'value' in cell) {
|
||||
formattedValues.push(cell);
|
||||
}
|
||||
}
|
||||
if ((typeof cell === 'number' || typeof cell === 'string') && typeof field.relationTo === 'string') {
|
||||
formattedValues.push({
|
||||
value: cell,
|
||||
relationTo: field.relationTo,
|
||||
});
|
||||
}
|
||||
});
|
||||
getRelationships(formattedValues);
|
||||
setHasRequested(true);
|
||||
setValues(formattedValues);
|
||||
}
|
||||
}, [cellData, relationTo, field, collections]);
|
||||
}, [cellData, field, collections, isAboveViewport, routes.api, hasRequested, getRelationships]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{data}
|
||||
</React.Fragment>
|
||||
<div
|
||||
className={baseClass}
|
||||
ref={intersectionRef}
|
||||
>
|
||||
{values.map(({ relationTo, value }, i) => {
|
||||
const document = documents[relationTo][value];
|
||||
const relatedCollection = collections.find(({ slug }) => slug === relationTo);
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
{ document === false && `Untitled - ID: ${value}`}
|
||||
{ document === null && 'Loading...'}
|
||||
{ document && (
|
||||
document[relatedCollection.admin.useAsTitle] ? document[relatedCollection.admin.useAsTitle] : `Untitled - ID: ${value}`
|
||||
)}
|
||||
{values.length > i + 1 && ', '}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{ Array.isArray(cellData) && cellData.length > totalToShow && ` and ${cellData.length - totalToShow} more` }
|
||||
{ values.length === 0 && `No <${field.label}>`}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const UploadCell = ({ data }) => (
|
||||
<React.Fragment>
|
||||
<span>
|
||||
{data?.filename}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
export default UploadCell;
|
||||
|
||||
@@ -7,7 +7,6 @@ import relationship from './Relationship';
|
||||
import richText from './Richtext';
|
||||
import select from './Select';
|
||||
import textarea from './Textarea';
|
||||
import upload from './Upload';
|
||||
|
||||
|
||||
export default {
|
||||
@@ -20,5 +19,5 @@ export default {
|
||||
richText,
|
||||
select,
|
||||
textarea,
|
||||
upload,
|
||||
upload: relationship,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useConfig } from '../../../utilities/Config';
|
||||
import UploadGallery from '../../../elements/UploadGallery';
|
||||
import Eyebrow from '../../../elements/Eyebrow';
|
||||
@@ -13,6 +13,7 @@ import { Props } from './types';
|
||||
import ViewDescription from '../../../elements/ViewDescription';
|
||||
import PerPage from '../../../elements/PerPage';
|
||||
import { Gutter } from '../../../elements/Gutter';
|
||||
import { RelationshipProvider } from './RelationshipProvider';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -43,7 +44,6 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
|
||||
const { routes: { admin } } = useConfig();
|
||||
const history = useHistory();
|
||||
const { pathname, search } = useLocation();
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -73,14 +73,14 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
enableSort={Boolean(upload)}
|
||||
/>
|
||||
{(data.docs && data.docs.length > 0) && (
|
||||
<React.Fragment
|
||||
key={`${pathname}${search}`}
|
||||
>
|
||||
<React.Fragment>
|
||||
{!upload && (
|
||||
<RelationshipProvider>
|
||||
<Table
|
||||
data={data.docs}
|
||||
columns={tableColumns}
|
||||
/>
|
||||
</RelationshipProvider>
|
||||
)}
|
||||
{upload && (
|
||||
<UploadGallery
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useReducer, useRef } from 'react';
|
||||
import querystring from 'qs';
|
||||
import { useConfig } from '../../../../utilities/Config';
|
||||
import { TypeWithID } from '../../../../../../collections/config/types';
|
||||
import { reducer } from './reducer';
|
||||
import useDebounce from '../../../../../hooks/useDebounce';
|
||||
|
||||
// documents are first set to null when requested
|
||||
// set to false when no doc is returned
|
||||
// or set to the document returned
|
||||
export type Documents = {
|
||||
[slug: string]: {
|
||||
[id: string | number]: TypeWithID | null | false
|
||||
}
|
||||
}
|
||||
|
||||
type ListRelationshipContext = {
|
||||
getRelationships: (docs: {
|
||||
relationTo: string,
|
||||
value: number | string
|
||||
}[]) => void;
|
||||
documents: Documents
|
||||
}
|
||||
|
||||
const Context = createContext({} as ListRelationshipContext);
|
||||
|
||||
export const RelationshipProvider: React.FC<{children?: React.ReactNode}> = ({ children }) => {
|
||||
const [documents, dispatchDocuments] = useReducer(reducer, {});
|
||||
const debouncedDocuments = useDebounce(documents, 100);
|
||||
const config = useConfig();
|
||||
const {
|
||||
serverURL,
|
||||
routes: { api },
|
||||
} = config;
|
||||
|
||||
useEffect(() => {
|
||||
Object.entries(debouncedDocuments).forEach(async ([slug, docs]) => {
|
||||
const idsToLoad: (string | number)[] = [];
|
||||
|
||||
Object.entries(docs).forEach(([id, value]) => {
|
||||
if (value === null) {
|
||||
idsToLoad.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
if (idsToLoad.length > 0) {
|
||||
const url = `${serverURL}${api}/${slug}`;
|
||||
const params = {
|
||||
depth: 0,
|
||||
'where[id][in]': idsToLoad,
|
||||
pagination: false,
|
||||
};
|
||||
|
||||
const query = querystring.stringify(params, { addQueryPrefix: true });
|
||||
const result = await fetch(`${url}${query}`);
|
||||
if (result.ok) {
|
||||
const json = await result.json();
|
||||
if (json.docs) {
|
||||
dispatchDocuments({ type: 'ADD_LOADED', docs: json.docs, relationTo: slug, idsToLoad });
|
||||
}
|
||||
} else {
|
||||
dispatchDocuments({ type: 'ADD_LOADED', docs: [], relationTo: slug, idsToLoad });
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [serverURL, api, debouncedDocuments]);
|
||||
|
||||
const getRelationships = useCallback(async (relationships: { relationTo: string, value: number | string }[]) => {
|
||||
dispatchDocuments({ type: 'REQUEST', docs: relationships });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ getRelationships, documents }}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useListRelationships = (): ListRelationshipContext => useContext(Context);
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Documents } from './index';
|
||||
import { TypeWithID } from '../../../../../../collections/config/types';
|
||||
|
||||
type RequestDocuments = {
|
||||
type: 'REQUEST',
|
||||
docs: { relationTo: string, value: number | string }[],
|
||||
}
|
||||
|
||||
type AddLoadedDocuments = {
|
||||
type: 'ADD_LOADED',
|
||||
relationTo: string,
|
||||
docs: TypeWithID[],
|
||||
idsToLoad: (string | number)[]
|
||||
}
|
||||
|
||||
type Action = RequestDocuments | AddLoadedDocuments;
|
||||
|
||||
export function reducer(state: Documents, action: Action): Documents {
|
||||
switch (action.type) {
|
||||
case 'REQUEST': {
|
||||
const newState = { ...state };
|
||||
|
||||
action.docs.forEach(({ relationTo, value }) => {
|
||||
if (typeof newState[relationTo] !== 'object') {
|
||||
newState[relationTo] = {};
|
||||
}
|
||||
newState[relationTo][value] = null;
|
||||
});
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'ADD_LOADED': {
|
||||
const newState = { ...state };
|
||||
if (typeof newState[action.relationTo] !== 'object') {
|
||||
newState[action.relationTo] = {};
|
||||
}
|
||||
const unreturnedIDs = [...action.idsToLoad];
|
||||
|
||||
if (Array.isArray(action.docs)) {
|
||||
action.docs.forEach((doc) => {
|
||||
unreturnedIDs.splice(unreturnedIDs.indexOf(doc.id), 1);
|
||||
newState[action.relationTo][doc.id] = doc;
|
||||
});
|
||||
}
|
||||
|
||||
unreturnedIDs.forEach((id) => {
|
||||
newState[action.relationTo][id] = false;
|
||||
});
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,7 @@ const buildColumns = (collection: SanitizedCollectionConfig, columns: string[]):
|
||||
),
|
||||
renderCell: (rowData, cellData) => (
|
||||
<Cell
|
||||
key={JSON.stringify(cellData)}
|
||||
field={field}
|
||||
colIndex={colIndex}
|
||||
collection={collection}
|
||||
|
||||
@@ -76,7 +76,7 @@ const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
const params = {
|
||||
depth: 1,
|
||||
depth: 0,
|
||||
draft: 'true',
|
||||
page: undefined,
|
||||
sort: undefined,
|
||||
@@ -107,7 +107,7 @@ const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
setTableColumns(buildColumns(collection, currentPreferences?.columns));
|
||||
}
|
||||
|
||||
const params = queryString.parse(history.location.search, { ignoreQueryPrefix: true, depth: 10 });
|
||||
const params = queryString.parse(history.location.search, { ignoreQueryPrefix: true, depth: 0 });
|
||||
|
||||
const search = {
|
||||
...params,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { UploadedFile } from 'express-fileupload';
|
||||
import { Payload } from '../../..';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { Document } from '../../../types';
|
||||
|
||||
Reference in New Issue
Block a user