Merge branch 'master' of github.com:payloadcms/payload into fix/relationship-access-missing-id

This commit is contained in:
Jarrod Flesch
2021-10-01 17:08:37 -04:00
236 changed files with 5089 additions and 2370 deletions

View File

@@ -5,6 +5,7 @@ import { Props } from './types';
import plus from '../../icons/Plus';
import x from '../../icons/X';
import chevron from '../../icons/Chevron';
import edit from '../../icons/Edit';
import './index.scss';
@@ -12,6 +13,7 @@ const icons = {
plus,
x,
chevron,
edit,
};
const baseClass = 'btn';

View File

@@ -9,7 +9,7 @@ export type Props = {
children?: React.ReactNode,
onClick?: (event: MouseEvent) => void,
disabled?: boolean,
icon?: React.ReactNode | ['chevron' | 'x' | 'plus'],
icon?: React.ReactNode | ['chevron' | 'x' | 'plus' | 'edit'],
iconStyle?: 'with-border' | 'without-border' | 'none',
buttonStyle?: 'primary' | 'secondary' | 'transparent' | 'error' | 'none' | 'icon-label',
round?: boolean,

View File

@@ -1,6 +1,6 @@
import { CollectionConfig } from '../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
export type Props = {
collection: CollectionConfig,
collection: SanitizedCollectionConfig,
handleChange: (columns) => void,
}

View File

@@ -1,7 +1,7 @@
import { CollectionConfig } from '../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
export type Props = {
collection?: CollectionConfig,
collection?: SanitizedCollectionConfig,
id?: string,
title?: string,
}

View File

@@ -1,7 +1,7 @@
import { CollectionConfig } from '../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
export type Props = {
collection: CollectionConfig
collection: SanitizedCollectionConfig
doc: Record<string, unknown>
handleRemove?: () => void,
}

View File

@@ -1,10 +1,10 @@
import { CollectionConfig } from '../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
export type Props = {
enableColumns?: boolean,
enableSort?: boolean,
setSort: (sort: string) => void,
collection: CollectionConfig,
collection: SanitizedCollectionConfig,
handleChange: (newState) => void,
}

View File

@@ -130,7 +130,7 @@
backdrop-filter: saturate(180%) blur(5px);
width: 100%;
height: base(3);
z-index: $z-nav;
z-index: $z-modal;
&__scroll {
padding: 0;

View File

@@ -12,6 +12,7 @@ const baseClass = 'popup';
const Popup: React.FC<Props> = (props) => {
const {
className,
render,
size = 'small',
color = 'light',
@@ -87,6 +88,7 @@ const Popup: React.FC<Props> = (props) => {
const classes = [
baseClass,
className,
`${baseClass}--size-${size}`,
`${baseClass}--color-${color}`,
`${baseClass}--v-align-${verticalAlign}`,

View File

@@ -1,4 +1,5 @@
export type Props = {
className?: string
render?: (any) => void,
children?: React.ReactNode,
horizontalAlign?: 'left' | 'center' | 'right',

View File

@@ -7,6 +7,7 @@ import './index.scss';
const ReactSelect: React.FC<Props> = (props) => {
const {
className,
showError = false,
options,
onChange,
@@ -15,6 +16,7 @@ const ReactSelect: React.FC<Props> = (props) => {
} = props;
const classes = [
className,
'react-select',
showError && 'react-select--error',
].filter(Boolean).join(' ');

View File

@@ -7,6 +7,7 @@ export type Value = {
}
export type Props = {
className?: string
value?: Value | Value[],
onChange?: (value: any) => void,
disabled?: boolean,

View File

@@ -1,6 +1,6 @@
import { CollectionConfig } from '../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
export type Props = {
handleChange: (controls: any) => void,
collection: CollectionConfig,
collection: SanitizedCollectionConfig,
}

View File

@@ -1,7 +1,7 @@
import { CollectionConfig } from '../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
export type Props = {
doc: Record<string, unknown>
collection: CollectionConfig
collection: SanitizedCollectionConfig
size?: 'small' | 'medium' | 'large' | 'expand',
}

View File

@@ -9,6 +9,7 @@ const baseClass = 'upload-card';
const UploadCard: React.FC<Props> = (props) => {
const {
className,
onClick,
doc,
collection,
@@ -16,6 +17,7 @@ const UploadCard: React.FC<Props> = (props) => {
const classes = [
baseClass,
className,
typeof onClick === 'function' && `${baseClass}--has-on-click`,
].filter(Boolean).join(' ');

View File

@@ -1,7 +1,8 @@
import { CollectionConfig } from '../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
export type Props = {
collection: CollectionConfig,
className?: string
collection: SanitizedCollectionConfig,
doc: Record<string, unknown>
onClick?: () => void,
}

View File

@@ -1,7 +1,7 @@
import { CollectionConfig } from '../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
export type Props = {
docs?: Record<string, unknown>[],
collection: CollectionConfig,
collection: SanitizedCollectionConfig,
onCardClick: (doc) => void,
}

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { Props, isComponent } from './types';
import './index.scss';
const ViewDescription: React.FC<Props> = (props) => {
const {
description,
} = props;
if (isComponent(description)) {
const Description = description;
return <Description />;
}
if (description) {
return (
<div
className="view-description"
>
{typeof description === 'function' ? description() : description}
</div>
);
}
return null;
};
export default ViewDescription;

View File

@@ -0,0 +1,15 @@
import React from 'react';
export type DescriptionFunction = () => string
export type DescriptionComponent = React.ComponentType
type Description = string | DescriptionFunction | DescriptionComponent
export type Props = {
description?: Description
}
export function isComponent(description: Description): description is DescriptionComponent {
return React.isValidElement(description);
}

View File

@@ -45,6 +45,18 @@ const numeric = [
},
];
const geo = [
...boolean,
{
label: 'exists',
value: 'exists',
},
{
label: 'near',
value: 'near',
},
];
const like = {
label: 'is like',
value: 'like',
@@ -79,6 +91,10 @@ const fieldTypeConditions = {
component: 'Date',
operators: [...base, ...numeric],
},
point: {
component: 'Point',
operators: [...geo],
},
upload: {
component: 'Text',
operators: [...base],

View File

@@ -1,10 +1,10 @@
import { CollectionConfig } from '../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
import { Field } from '../../../../fields/config/types';
import { Operator } from '../../../../types';
export type Props = {
handleChange: (controls: any) => void,
collection: CollectionConfig,
collection: SanitizedCollectionConfig,
}
export type FieldCondition = {

View File

@@ -0,0 +1,8 @@
@import '../../../scss/styles.scss';
.field-description {
display: flex;
padding-top: base(.25);
padding-bottom: base(.25);
color: $color-gray;
}

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { Props, isComponent } from './types';
import './index.scss';
const FieldDescription: React.FC<Props> = (props) => {
const {
description,
value,
} = props;
if (isComponent(description)) {
const Description = description;
return <Description value={value} />;
}
if (description) {
return (
<div
className="field-description"
>
{typeof description === 'function' ? description({ value }) : description}
</div>
);
}
return null;
};
export default FieldDescription;

View File

@@ -0,0 +1,16 @@
import React from 'react';
export type DescriptionFunction = (value: unknown) => string
export type DescriptionComponent = React.ComponentType<{value: unknown}>
type Description = string | DescriptionFunction | DescriptionComponent
export type Props = {
description?: Description
value: unknown;
}
export function isComponent(description: Description): description is DescriptionComponent {
return React.isValidElement(description);
}

View File

@@ -2,18 +2,12 @@ import ObjectID from 'bson-objectid';
import { Field as FieldSchema } from '../../../../fields/config/types';
import { Fields, Field, Data } from './types';
const buildValidationPromise = async (fieldState: Field, field: FieldSchema, fullData: Data = {}, data: Data = {}) => {
const buildValidationPromise = async (fieldState: Field, field: FieldSchema) => {
const validatedFieldState = fieldState;
let passesConditionalLogic = true;
if (field?.admin?.condition) {
passesConditionalLogic = await field.admin.condition(fullData, data);
}
let validationResult: boolean | string = true;
if (passesConditionalLogic && typeof field.validate === 'function') {
if (typeof field.validate === 'function') {
validationResult = await field.validate(fieldState.value, field);
}
@@ -29,7 +23,7 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data =
if (fieldSchema) {
const validationPromises = [];
const structureFieldState = (field, data = {}) => {
const structureFieldState = (field, passesCondition, data = {}) => {
const value = typeof data?.[field.name] !== 'undefined' ? data[field.name] : field.defaultValue;
const fieldState = {
@@ -37,15 +31,16 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data =
initialValue: value,
valid: true,
validate: field.validate,
condition: field?.admin?.condition,
condition: field.admin?.condition,
passesCondition,
};
validationPromises.push(buildValidationPromise(fieldState, field, fullData, data));
validationPromises.push(buildValidationPromise(fieldState, field));
return fieldState;
};
const iterateFields = (fields: FieldSchema[], data: Data, path = '') => fields.reduce((state, field) => {
const iterateFields = (fields: FieldSchema[], data: Data, parentPassesCondition: boolean, path = '') => fields.reduce((state, field) => {
let initialData = data;
if (!field?.admin?.disabled) {
@@ -53,6 +48,8 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data =
initialData = { [field.name]: field.defaultValue };
}
const passesCondition = Boolean((field?.admin?.condition ? field.admin.condition(fullData || {}, initialData || {}) : true) && parentPassesCondition);
if (field.name) {
if (field.type === 'relationship' && initialData?.[field.name] === null) {
initialData[field.name] = 'null';
@@ -75,7 +72,7 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data =
initialValue: row.id || new ObjectID().toHexString(),
valid: true,
},
...iterateFields(field.fields, row, rowPath),
...iterateFields(field.fields, row, passesCondition, rowPath),
};
}, {}),
};
@@ -104,7 +101,7 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data =
initialValue: row.id || new ObjectID().toHexString(),
valid: true,
},
...(block?.fields ? iterateFields(block.fields, row, rowPath) : {}),
...(block?.fields ? iterateFields(block.fields, row, passesCondition, rowPath) : {}),
};
}, {}),
};
@@ -120,13 +117,13 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data =
return {
...state,
...iterateFields(field.fields, subFieldData, `${path}${field.name}.`),
...iterateFields(field.fields, subFieldData, passesCondition, `${path}${field.name}.`),
};
}
return {
...state,
[`${path}${field.name}`]: structureFieldState(field, data),
[`${path}${field.name}`]: structureFieldState(field, passesCondition, data),
};
}
@@ -134,21 +131,21 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data =
if (field.type === 'row') {
return {
...state,
...iterateFields(field.fields, data, path),
...iterateFields(field.fields, data, passesCondition, path),
};
}
// Handle normal fields
return {
...state,
[`${path}${field.name}`]: structureFieldState(field, data),
[`${path}${field.name}`]: structureFieldState(field, passesCondition, data),
};
}
return state;
}, {});
const resultingState = iterateFields(fieldSchema, fullData);
const resultingState = iterateFields(fieldSchema, fullData, true);
await Promise.all(validationPromises);
return resultingState;
}

View File

@@ -1,5 +1,8 @@
import equal from 'deep-equal';
import { unflatten, flatten } from 'flatley';
import flattenFilters from './flattenFilters';
import getSiblingData from './getSiblingData';
import reduceFieldsToValues from './reduceFieldsToValues';
import { Fields } from './types';
const unflattenRowsFromState = (state: Fields, path) => {
@@ -36,7 +39,26 @@ const unflattenRowsFromState = (state: Fields, path) => {
function fieldReducer(state: Fields, action): Fields {
switch (action.type) {
case 'REPLACE_STATE': {
return action.state;
const newState = {};
// Only update fields that have changed
// by comparing old value / initialValue to new
// ..
// This is a performance enhancement for saving
// large documents with hundreds of fields
Object.entries(action.state).forEach(([path, field]) => {
const oldField = state[path];
const newField = field;
if (!equal(oldField, newField)) {
newState[path] = newField;
} else if (oldField) {
newState[path] = oldField;
}
});
return newState;
}
case 'REMOVE': {
@@ -103,6 +125,39 @@ function fieldReducer(state: Fields, action): Fields {
return newState;
}
case 'MODIFY_CONDITION': {
const { path, result } = action;
return Object.entries(state).reduce((newState, [fieldPath, field]) => {
if (fieldPath === path || fieldPath.indexOf(`${path}.`) === 0) {
let passesCondition = result;
// If a condition is being set to true,
// Set all conditions to true
// Besides those who still fail their own conditions
if (passesCondition && field.condition) {
passesCondition = field.condition(reduceFieldsToValues(state), getSiblingData(state, path));
}
return {
...newState,
[fieldPath]: {
...field,
passesCondition,
},
};
}
return {
...newState,
[fieldPath]: {
...field,
},
};
}, {});
}
default: {
const newField = {
value: action.value,
@@ -114,6 +169,7 @@ function fieldReducer(state: Fields, action): Fields {
stringify: action.stringify,
validate: action.validate,
condition: action.condition,
passesCondition: action.passesCondition,
};
return {

View File

@@ -67,32 +67,24 @@ const Form: React.FC<Props> = (props) => {
const validatedFieldState = {};
let isValid = true;
const data = contextRef.current.getData();
const validationPromises = Object.entries(contextRef.current.fields).map(async ([path, field]) => {
const validatedField = {
...field,
valid: true,
};
const siblingData = contextRef.current.getSiblingData(path);
if (field.passesCondition !== false) {
let validationResult: boolean | string = true;
let passesConditionalLogic = true;
if (typeof field.validate === 'function') {
validationResult = await field.validate(field.value);
}
if (typeof field?.condition === 'function') {
passesConditionalLogic = await field.condition(data, siblingData);
}
let validationResult: boolean | string = true;
if (passesConditionalLogic && typeof field.validate === 'function') {
validationResult = await field.validate(field.value);
}
if (typeof validationResult === 'string') {
validatedField.errorMessage = validationResult;
validatedField.valid = false;
isValid = false;
if (typeof validationResult === 'string') {
validatedField.errorMessage = validationResult;
validatedField.valid = false;
isValid = false;
}
}
validatedFieldState[path] = validatedField;

View File

@@ -10,6 +10,7 @@ export type Field = {
ignoreWhileFlattening?: boolean
stringify?: boolean
condition?: Condition
passesCondition?: boolean
}
export type Fields = {

View File

@@ -3,7 +3,7 @@
label.field-label {
display: flex;
padding-bottom: base(.25);
color: $color-gray;
color: $color-dark-gray;
.required {
color: $color-red;

View File

@@ -11,6 +11,7 @@ import useFieldType from '../../useFieldType';
import Error from '../../Error';
import { array } from '../../../../../fields/validations';
import Banner from '../../../elements/Banner';
import FieldDescription from '../../FieldDescription';
import { Props, RenderArrayProps } from './types';
import './index.scss';
@@ -30,6 +31,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
permissions,
admin: {
readOnly,
description,
condition,
},
} = props;
@@ -132,6 +134,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
minRows={minRows}
maxRows={maxRows}
required={required}
description={description}
/>
);
};
@@ -156,6 +159,7 @@ const RenderArray = React.memo((props: RenderArrayProps) => {
minRows,
maxRows,
required,
description,
} = props;
const hasMaxRows = maxRows && rows.length >= maxRows;
@@ -173,6 +177,10 @@ const RenderArray = React.memo((props: RenderArrayProps) => {
</div>
<header className={`${baseClass}__header`}>
<h3>{label}</h3>
<FieldDescription
value={value}
description={description}
/>
</header>
<Droppable droppableId="array-drop">
{(provided) => (

View File

@@ -4,6 +4,14 @@
margin: base(2) 0;
min-width: base(15);
&__header {
h3 {
margin-bottom: 0;
}
margin-bottom: base(1);
}
&__error-wrap {
position: relative;
}

View File

@@ -1,5 +1,5 @@
import { Data } from '../../Form/types';
import { ArrayField, Labels, Field } from '../../../../../fields/config/types';
import { ArrayField, Labels, Field, Description } from '../../../../../fields/config/types';
import { FieldTypes } from '..';
import { FieldPermissions } from '../../../../../auth/types';
@@ -30,4 +30,5 @@ export type RenderArrayProps = {
showError: boolean
errorMessage: string
rows: Data[]
description?: Description
}

View File

@@ -17,6 +17,7 @@ import Popup from '../../../elements/Popup';
import BlockSelector from './BlockSelector';
import { blocks as blocksValidator } from '../../../../../fields/validations';
import Banner from '../../../elements/Banner';
import FieldDescription from '../../FieldDescription';
import { Props, RenderBlockProps } from './types';
import { DocumentPreferences } from '../../../../../preferences/types';
@@ -44,6 +45,7 @@ const Blocks: React.FC<Props> = (props) => {
permissions,
admin: {
readOnly,
description,
condition,
},
} = props;
@@ -181,6 +183,7 @@ const Blocks: React.FC<Props> = (props) => {
minRows={minRows}
maxRows={maxRows}
required={required}
description={description}
/>
);
};
@@ -206,6 +209,7 @@ const RenderBlocks = React.memo((props: RenderBlockProps) => {
minRows,
maxRows,
required,
description,
} = props;
const hasMaxRows = maxRows && rows.length >= maxRows;
@@ -223,6 +227,10 @@ const RenderBlocks = React.memo((props: RenderBlockProps) => {
</div>
<header className={`${baseClass}__header`}>
<h3>{label}</h3>
<FieldDescription
value={value}
description={description}
/>
</header>
<Droppable

View File

@@ -4,6 +4,14 @@
margin: base(2) 0;
min-width: base(15);
&__header {
h3 {
margin-bottom: 0;
}
margin-bottom: base(1);
}
&__error-wrap {
position: relative;
}

View File

@@ -1,5 +1,5 @@
import { Data } from '../../Form/types';
import { BlockField, Labels, Block } from '../../../../../fields/config/types';
import { BlockField, Labels, Block, Description } from '../../../../../fields/config/types';
import { FieldTypes } from '..';
import { FieldPermissions } from '../../../../../auth/types';
@@ -28,6 +28,7 @@ export type RenderBlockProps = {
showError: boolean
errorMessage: string
rows: Data[]
blocks: Block[],
blocks: Block[]
setCollapse: (id: string, collapsed: boolean) => void
description?: Description
}

View File

@@ -4,6 +4,7 @@ import withCondition from '../../withCondition';
import Error from '../../Error';
import { checkbox } from '../../../../../fields/validations';
import Check from '../../../icons/Check';
import FieldDescription from '../../FieldDescription';
import { Props } from './types';
import './index.scss';
@@ -23,6 +24,7 @@ const Checkbox: React.FC<Props> = (props) => {
readOnly,
style,
width,
description,
condition,
} = {},
} = props;
@@ -87,6 +89,10 @@ const Checkbox: React.FC<Props> = (props) => {
{label}
</span>
</button>
<FieldDescription
value={value}
description={description}
/>
</div>
);
};

View File

@@ -7,6 +7,7 @@ import useFieldType from '../../useFieldType';
import withCondition from '../../withCondition';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
import { code } from '../../../../../fields/validations';
import { Props } from './types';
@@ -23,6 +24,7 @@ const Code: React.FC<Props> = (props) => {
style,
width,
language,
description,
condition,
} = {},
label,
@@ -94,6 +96,10 @@ const Code: React.FC<Props> = (props) => {
pointerEvents: readOnly ? 'none' : 'auto',
}}
/>
<FieldDescription
value={value}
description={description}
/>
</div>
);
};

View File

@@ -3,6 +3,10 @@
.date-time-field {
margin-bottom: $baseline;
&__error-wrap {
position: relative;
}
&--has-error {
.react-datepicker__input-container input {
background-color: lighten($color-red, 20%);

View File

@@ -5,6 +5,7 @@ import withCondition from '../../withCondition';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
import { date as dateValidation } from '../../../../../fields/validations';
import { Props } from './types';
@@ -25,6 +26,7 @@ const DateTime: React.FC<Props> = (props) => {
style,
width,
date,
description,
condition,
} = {},
} = props;
@@ -62,10 +64,12 @@ const DateTime: React.FC<Props> = (props) => {
width,
}}
>
<Error
showError={showError}
message={errorMessage}
/>
<div className={`${baseClass}__error-wrap`}>
<Error
showError={showError}
message={errorMessage}
/>
</div>
<Label
htmlFor={path}
label={label}
@@ -80,6 +84,10 @@ const DateTime: React.FC<Props> = (props) => {
value={value as Date}
/>
</div>
<FieldDescription
value={value}
description={description}
/>
</div>
);
};

View File

@@ -3,6 +3,7 @@ import withCondition from '../../withCondition';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
import { email } from '../../../../../fields/validations';
import { Props } from './types';
@@ -20,6 +21,7 @@ const Email: React.FC<Props> = (props) => {
width,
placeholder,
autoComplete,
description,
condition,
} = {},
label,
@@ -80,6 +82,10 @@ const Email: React.FC<Props> = (props) => {
name={path}
autoComplete={autoComplete}
/>
<FieldDescription
value={value}
description={description}
/>
</div>
);
};

View File

@@ -4,6 +4,15 @@
margin-top: base(2);
margin-bottom: base(2);
display: flex;
position: relative;
&__header {
margin-bottom: base(1);
}
&__title {
margin-bottom: 0;
}
&:hover {
.field-type-gutter__content-container {
@@ -29,6 +38,10 @@
}
}
&--no-label {
margin-top: 0,
}
@include mid-break {
&__fields-wrapper {
display: flex;

View File

@@ -1,6 +1,7 @@
import React from 'react';
import RenderFields from '../../RenderFields';
import withCondition from '../../withCondition';
import FieldDescription from '../../FieldDescription';
import FieldTypeGutter from '../../FieldTypeGutter';
import { NegativeFieldGutterProvider } from '../../FieldTypeGutter/context';
import { Props } from './types';
@@ -21,6 +22,7 @@ const Group: React.FC<Props> = (props) => {
style,
width,
hideGutter,
description,
},
permissions,
} = props;
@@ -29,7 +31,11 @@ const Group: React.FC<Props> = (props) => {
return (
<div
className="field-type group"
className={[
'field-type',
baseClass,
!label && `${baseClass}--no-label`,
].filter(Boolean).join(' ')}
style={{
...style,
width,
@@ -38,8 +44,16 @@ const Group: React.FC<Props> = (props) => {
{ !hideGutter && (<FieldTypeGutter />) }
<div className={`${baseClass}__content-wrapper`}>
{label && (
<h2 className={`${baseClass}__title`}>{label}</h2>
{(label || description) && (
<header className={`${baseClass}__header`}>
{label && (
<h3 className={`${baseClass}__title`}>{label}</h3>
)}
<FieldDescription
value={null}
description={description}
/>
</header>
)}
<div className={`${baseClass}__fields-wrapper`}>
<NegativeFieldGutterProvider allow={false}>

View File

@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
import withCondition from '../../withCondition';
import { number } from '../../../../../fields/validations';
import { Props } from './types';
@@ -23,6 +24,7 @@ const NumberField: React.FC<Props> = (props) => {
width,
step,
placeholder,
description,
condition,
} = {},
} = props;
@@ -90,6 +92,10 @@ const NumberField: React.FC<Props> = (props) => {
name={path}
step={step}
/>
<FieldDescription
value={value}
description={description}
/>
</div>
);
};

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Validate } from '../../../../../fields/config/types';
import { Description, Validate } from '../../../../../fields/config/types';
export type Props = {
autoComplete?: string
@@ -10,4 +10,5 @@ export type Props = {
style?: React.CSSProperties
width?: string
label?: string
description?: Description
}

View File

@@ -0,0 +1,30 @@
@import '../../../../scss/styles.scss';
.point {
position: relative;
margin-bottom: $baseline;
&__wrap {
display: flex;
width: calc(100% + #{base(1)});
margin-left: base(-.5);
margin-right: base(-.5);
list-style: none;
padding: 0;
li {
padding: 0 base(.5);
width: 50%;
}
}
input {
@include formInput;
}
&.error {
input {
background-color: lighten($color-red, 20%);
}
}
}

View File

@@ -0,0 +1,124 @@
import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
import withCondition from '../../withCondition';
import { point } from '../../../../../fields/validations';
import { Props } from './types';
import './index.scss';
const baseClass = 'point';
const PointField: React.FC<Props> = (props) => {
const {
name,
path: pathFromProps,
required,
validate = point,
label,
admin: {
readOnly,
style,
width,
step,
placeholder,
description,
condition,
} = {},
} = props;
const path = pathFromProps || name;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
return validationResult;
}, [validate, required]);
const {
value = [null, null],
showError,
setValue,
errorMessage,
} = useFieldType<[number, number]>({
path,
validate: memoizedValidate,
enableDebouncedValue: true,
condition,
});
const handleChange = useCallback((e, index: 0 | 1) => {
let val = parseFloat(e.target.value);
if (Number.isNaN(val)) {
val = e.target.value;
}
const coordinates = [...value];
coordinates[index] = val;
setValue(coordinates);
}, [setValue, value]);
const classes = [
'field-type',
baseClass,
showError && 'error',
readOnly && 'read-only',
].filter(Boolean).join(' ');
return (
<div
className={classes}
style={{
...style,
width,
}}
>
<Error
showError={showError}
message={errorMessage}
/>
<ul className={`${baseClass}__wrap`}>
<li>
<Label
htmlFor={`${path}.longitude`}
label={`${label} - Longitude`}
required={required}
/>
<input
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`}
label={`${label} - Latitude`}
required={required}
/>
<input
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}
/>
</li>
</ul>
<FieldDescription
value={value}
description={description}
/>
</div>
);
};
export default withCondition(PointField);

View File

@@ -0,0 +1,5 @@
import { NumberField } from '../../../../../fields/config/types';
export type Props = Omit<NumberField, 'type'> & {
path?: string
}

View File

@@ -64,13 +64,13 @@
cursor: default;
&__label {
color: $color-gray;
color: $color-dark-gray;
}
&--is-selected {
.radio-input__styled-radio {
&:before {
background-color: $color-gray;
background-color: $color-dark-gray;
}
}
}

View File

@@ -4,6 +4,7 @@ import useFieldType from '../../useFieldType';
import withCondition from '../../withCondition';
import Error from '../../Error';
import Label from '../../Label';
import FieldDescription from '../../FieldDescription';
import RadioInput from './RadioInput';
import { radio } from '../../../../../fields/validations';
import { optionIsObject } from '../../../../../fields/config/types';
@@ -25,6 +26,7 @@ const RadioGroup: React.FC<Props> = (props) => {
layout = 'horizontal',
style,
width,
description,
condition,
} = {},
options,
@@ -99,6 +101,10 @@ const RadioGroup: React.FC<Props> = (props) => {
);
})}
</ul>
<FieldDescription
value={value}
description={description}
/>
</div>
);
};

View File

@@ -9,6 +9,7 @@ import { Value } from '../../../elements/ReactSelect/types';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
import { relationship } from '../../../../../fields/validations';
import { PaginatedDocs } from '../../../../../collections/config/types';
import { useFormProcessing } from '../../Form/context';
@@ -35,6 +36,7 @@ const Relationship: React.FC<Props> = (props) => {
readOnly,
style,
width,
description,
condition,
} = {},
} = props;
@@ -393,6 +395,10 @@ const Relationship: React.FC<Props> = (props) => {
{errorLoading}
</div>
)}
<FieldDescription
value={value}
description={description}
/>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import { PaginatedDocs, CollectionConfig } from '../../../../../collections/config/types';
import { PaginatedDocs, SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { RelationshipField } from '../../../../../fields/config/types';
export type OptionsPage = {
@@ -27,7 +27,7 @@ type ADD = {
data: PaginatedDocs
relation: string
hasMultipleRelations: boolean
collection: CollectionConfig
collection: SanitizedCollectionConfig
}
export type Action = CLEAR | ADD

View File

@@ -1,8 +1,8 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import isHotkey from 'is-hotkey';
import { Editable, withReact, Slate } from 'slate-react';
import { createEditor, Transforms, Node } from 'slate';
import { withHistory } from 'slate-history';
import { createEditor, Transforms, Node, Element as SlateElement, Text, BaseEditor } from 'slate';
import { ReactEditor, Editable, withReact, Slate } from 'slate-react';
import { HistoryEditor, withHistory } from 'slate-history';
import { richText } from '../../../../../fields/validations';
import useFieldType from '../../useFieldType';
import withCondition from '../../withCondition';
@@ -15,6 +15,7 @@ import hotkeys from './hotkeys';
import enablePlugins from './enablePlugins';
import defaultValue from '../../../../../fields/richText/defaultValue';
import FieldTypeGutter from '../../FieldTypeGutter';
import FieldDescription from '../../FieldDescription';
import withHTML from './plugins/withHTML';
import { Props } from './types';
import { RichTextElement, RichTextLeaf } from '../../../../../fields/config/types';
@@ -24,11 +25,22 @@ import mergeCustomFunctions from './mergeCustomFunctions';
import './index.scss';
const defaultElements: RichTextElement[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'link', 'relationship'];
const defaultElements: RichTextElement[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'link', 'relationship', 'upload'];
const defaultLeaves: RichTextLeaf[] = ['bold', 'italic', 'underline', 'strikethrough', 'code'];
const enterBreakOutTypes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
const baseClass = 'rich-text';
type CustomText = { text: string; [x: string]: unknown }
type CustomElement = { type: string; children: CustomText[] }
declare module 'slate' {
interface CustomTypes {
Editor: BaseEditor & ReactEditor & HistoryEditor
Element: CustomElement
Text: CustomText
}
}
const RichText: React.FC<Props> = (props) => {
const {
@@ -43,8 +55,9 @@ const RichText: React.FC<Props> = (props) => {
style,
width,
placeholder,
condition,
description,
hideGutter,
condition,
} = {},
} = props;
@@ -66,6 +79,7 @@ const RichText: React.FC<Props> = (props) => {
<Element
attributes={attributes}
element={element}
path={path}
>
{children}
</Element>
@@ -73,7 +87,7 @@ const RichText: React.FC<Props> = (props) => {
}
return <div {...attributes}>{children}</div>;
}, [enabledElements]);
}, [enabledElements, path]);
const renderLeaf = useCallback(({ attributes, children, leaf }) => {
const matchedLeafName = Object.keys(enabledLeaves).find((leafName) => leaf[leafName]);
@@ -85,6 +99,7 @@ const RichText: React.FC<Props> = (props) => {
<Leaf
attributes={attributes}
leaf={leaf}
path={path}
>
{children}
</Leaf>
@@ -94,7 +109,7 @@ const RichText: React.FC<Props> = (props) => {
return (
<span {...attributes}>{children}</span>
);
}, [enabledLeaves]);
}, [enabledLeaves, path]);
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
@@ -177,8 +192,8 @@ const RichText: React.FC<Props> = (props) => {
width,
}}
>
{ !hideGutter && (<FieldTypeGutter />) }
<div className={`${baseClass}__wrap`}>
{ !hideGutter && (<FieldTypeGutter />) }
<Error
showError={showError}
message={errorMessage}
@@ -254,19 +269,21 @@ const RichText: React.FC<Props> = (props) => {
} else {
const selectedElement = Node.descendant(editor, editor.selection.anchor.path.slice(0, -1));
// Allow hard enter to "break out" of certain elements
if (enterBreakOutTypes.includes(String(selectedElement.type))) {
event.preventDefault();
const selectedLeaf = Node.descendant(editor, editor.selection.anchor.path);
if (SlateElement.isElement(selectedElement)) {
// Allow hard enter to "break out" of certain elements
if (enterBreakOutTypes.includes(String(selectedElement.type))) {
event.preventDefault();
const selectedLeaf = Node.descendant(editor, editor.selection.anchor.path);
if (String(selectedLeaf.text).length === editor.selection.anchor.offset) {
Transforms.insertNodes(editor, {
type: 'p',
children: [{ text: '', marks: [] }],
});
} else {
Transforms.splitNodes(editor);
Transforms.setNodes(editor, { type: 'p' });
if (Text.isText(selectedLeaf) && String(selectedLeaf.text).length === editor.selection.anchor.offset) {
Transforms.insertNodes(editor, {
type: 'p',
children: [{ text: '' }],
});
} else {
Transforms.splitNodes(editor);
Transforms.setNodes(editor, { type: 'p' });
}
}
}
}
@@ -275,16 +292,18 @@ const RichText: React.FC<Props> = (props) => {
if (event.key === 'Backspace') {
const selectedElement = Node.descendant(editor, editor.selection.anchor.path.slice(0, -1));
if (selectedElement.type === 'li') {
if (SlateElement.isElement(selectedElement) && selectedElement.type === 'li') {
const selectedLeaf = Node.descendant(editor, editor.selection.anchor.path);
if (String(selectedLeaf.text).length === 1) {
if (Text.isText(selectedLeaf) && String(selectedLeaf.text).length === 1) {
Transforms.unwrapNodes(editor, {
match: (n) => listTypes.includes(n.type as string),
match: (n) => SlateElement.isElement(n) && listTypes.includes(n.type),
split: true,
});
Transforms.setNodes(editor, { type: 'p' });
}
} else if (editor.isVoid(selectedElement)) {
Transforms.removeNodes(editor);
}
}
@@ -299,6 +318,10 @@ const RichText: React.FC<Props> = (props) => {
/>
</div>
</Slate>
<FieldDescription
value={value}
description={description}
/>
</div>
</div>
);

View File

@@ -9,6 +9,7 @@ import ol from './ol';
import ul from './ul';
import li from './li';
import relationship from './relationship';
import upload from './upload';
export default {
h1,
@@ -22,4 +23,5 @@ export default {
ul,
li,
relationship,
upload,
};

View File

@@ -1,8 +1,8 @@
import { Editor } from 'slate';
import { Editor, Element } from 'slate';
const isElementActive = (editor, format) => {
const [match] = Editor.nodes(editor, {
match: (n) => n.type === format,
match: (n) => Element.isElement(n) && n.type === format,
});
return !!match;

View File

@@ -1,12 +1,12 @@
import { Editor, Transforms, Range } from 'slate';
import { Editor, Transforms, Range, Element } from 'slate';
export const isLinkActive = (editor: Editor): boolean => {
const [link] = Editor.nodes(editor, { match: (n) => n.type === 'link' });
const [link] = Editor.nodes(editor, { match: (n) => Element.isElement(n) && n.type === 'link' });
return !!link;
};
export const unwrapLink = (editor: Editor): void => {
Transforms.unwrapNodes(editor, { match: (n) => n.type === 'link' });
Transforms.unwrapNodes(editor, { match: (n) => Element.isElement(n) && n.type === 'link' });
};
export const wrapLink = (editor: Editor, url?: string, newTab?: boolean): void => {

View File

@@ -31,18 +31,13 @@ const insertRelationship = (editor, { value, relationTo }) => {
],
};
const nodes = [relationship, { children: [{ text: '' }] }];
const nodes = [relationship, { type: 'p', children: [{ text: '' }] }];
if (editor.blurSelection) {
Transforms.select(editor, editor.blurSelection);
}
Transforms.insertNodes(editor, nodes);
const currentPath = editor.selection.anchor.path[0];
const newSelection = { anchor: { path: [currentPath + 1, 0], offset: 0 }, focus: { path: [currentPath + 1, 0], offset: 0 } };
Transforms.select(editor, newSelection);
ReactEditor.focus(editor);
};

View File

@@ -19,4 +19,9 @@
text-overflow: ellipsis;
overflow: hidden;
}
&--selected {
box-shadow: $focus-box-shadow;
outline: none;
}
}

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useFocused, useSelected } from 'slate-react';
import { useConfig } from '@payloadcms/config-provider';
import RelationshipIcon from '../../../../../../icons/Relationship';
import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
@@ -11,10 +12,13 @@ const initialParams = {
depth: 0,
};
const Element = ({ attributes, children, element }) => {
const Element = (props) => {
const { attributes, children, element } = props;
const { relationTo, value } = element;
const { collections, serverURL, routes: { api } } = useConfig();
const [relatedCollection] = useState(() => collections.find((coll) => coll.slug === relationTo));
const selected = useSelected();
const focused = useFocused();
const [{ data }] = usePayloadAPI(
`${serverURL}${api}/${relatedCollection.slug}/${value?.id}`,
@@ -23,7 +27,10 @@ const Element = ({ attributes, children, element }) => {
return (
<div
className={baseClass}
className={[
baseClass,
(selected && focused) && `${baseClass}--selected`,
].filter(Boolean).join(' ')}
contentEditable={false}
{...attributes}
>

View File

@@ -1,4 +1,4 @@
import { Transforms } from 'slate';
import { Element, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
import isElementActive from './isActive';
import listTypes from './listTypes';
@@ -20,7 +20,7 @@ const toggleElement = (editor, format) => {
}
Transforms.unwrapNodes(editor, {
match: (n) => listTypes.includes(n.type as string),
match: (n) => Element.isElement(n) && listTypes.includes(n.type as string),
split: true,
});

View File

@@ -0,0 +1,7 @@
@import '../../../../../../../scss/styles.scss';
.upload-rich-text-button {
.btn {
margin-right: base(1);
}
}

View File

@@ -0,0 +1,203 @@
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import { Modal, useModal } from '@faceless-ui/modal';
import { Transforms } from 'slate';
import { ReactEditor, useSlate } from 'slate-react';
import { useConfig } from '@payloadcms/config-provider';
import ElementButton from '../../Button';
import UploadIcon from '../../../../../../icons/Upload';
import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
import UploadGallery from '../../../../../../elements/UploadGallery';
import ListControls from '../../../../../../elements/ListControls';
import ReactSelect from '../../../../../../elements/ReactSelect';
import Paginator from '../../../../../../elements/Paginator';
import formatFields from '../../../../../../views/collections/List/formatFields';
import Label from '../../../../../Label';
import MinimalTemplate from '../../../../../../templates/Minimal';
import Button from '../../../../../../elements/Button';
import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types';
import './index.scss';
import '../modal.scss';
const baseClass = 'upload-rich-text-button';
const baseModalClass = 'rich-text-upload-modal';
const insertUpload = (editor, { value, relationTo }) => {
const text = { text: ' ' };
const upload = {
type: 'upload',
value,
relationTo,
children: [
text,
],
};
const nodes = [upload, { type: 'p', children: [{ text: '' }] }];
if (editor.blurSelection) {
Transforms.select(editor, editor.blurSelection);
}
Transforms.insertNodes(editor, nodes);
ReactEditor.focus(editor);
};
const UploadButton: React.FC<{path: string}> = ({ path }) => {
const { open, closeAll, currentModal } = useModal();
const editor = useSlate();
const { serverURL, routes: { api }, collections } = useConfig();
const [availableCollections] = useState(() => collections.filter(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
const [renderModal, setRenderModal] = useState(false);
const [modalCollectionOption, setModalCollectionOption] = useState<{ label: string, value: string}>(() => {
const firstAvailableCollection = collections.find(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship));
return { label: firstAvailableCollection.labels.singular, value: firstAvailableCollection.slug };
});
const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>(() => collections.find(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
const [listControls, setListControls] = useState<{where?: unknown}>({});
const [page, setPage] = useState(null);
const [sort, setSort] = useState(null);
const [fields, setFields] = useState(formatFields(modalCollection));
const [hasEnabledCollections] = useState(() => collections.find(({ upload, admin: { enableRichTextRelationship } }) => upload && enableRichTextRelationship));
const modalSlug = `${path}-add-upload`;
const moreThanOneAvailableCollection = availableCollections.length > 1;
const isOpen = currentModal === modalSlug;
// If modal is open, get active page of upload gallery
const apiURL = isOpen ? `${serverURL}${api}/${modalCollection.slug}` : null;
const [{ data }, { setParams }] = usePayloadAPI(apiURL, {});
useEffect(() => {
setFields(formatFields(modalCollection));
}, [modalCollection]);
useEffect(() => {
if (renderModal) {
open(modalSlug);
}
}, [renderModal, open, modalSlug]);
useEffect(() => {
const params: {
page?: number
sort?: string
where?: unknown
} = {};
if (page) params.page = page;
if (listControls?.where) params.where = listControls.where;
if (sort) params.sort = sort;
setParams(params);
}, [setParams, page, listControls, sort]);
useEffect(() => {
setModalCollection(collections.find(({ slug }) => modalCollectionOption.value === slug));
}, [modalCollectionOption, collections]);
if (!hasEnabledCollections) return null;
return (
<Fragment>
<ElementButton
className={baseClass}
format="upload"
onClick={() => setRenderModal(true)}
>
<UploadIcon />
</ElementButton>
{renderModal && (
<Modal
className={baseModalClass}
slug={modalSlug}
>
{isOpen && (
<MinimalTemplate width="wide">
<header className={`${baseModalClass}__header`}>
<h1>
Add
{' '}
{modalCollection.labels.singular}
</h1>
<Button
icon="x"
round
buttonStyle="icon-label"
iconStyle="with-border"
onClick={() => {
closeAll();
setRenderModal(false);
}}
/>
</header>
{moreThanOneAvailableCollection && (
<div className={`${baseModalClass}__select-collection-wrap`}>
<Label label="Select a Collection to Browse" />
<ReactSelect
className={`${baseClass}__select-collection`}
value={modalCollectionOption}
onChange={setModalCollectionOption}
options={availableCollections.map((coll) => ({ label: coll.labels.singular, value: coll.slug }))}
/>
</div>
)}
<ListControls
handleChange={setListControls}
collection={{
...modalCollection,
fields,
}}
enableColumns={false}
setSort={setSort}
enableSort
/>
<UploadGallery
docs={data?.docs}
collection={modalCollection}
onCardClick={(doc) => {
insertUpload(editor, {
value: {
id: doc.id,
},
relationTo: modalCollection.slug,
});
setRenderModal(false);
closeAll();
}}
/>
<div className={`${baseClass}__page-controls`}>
<Paginator
limit={data.limit}
totalPages={data.totalPages}
page={data.page}
hasPrevPage={data.hasPrevPage}
hasNextPage={data.hasNextPage}
prevPage={data.prevPage}
nextPage={data.nextPage}
numberOfNeighbors={1}
onChange={setPage}
disableHistoryChange
/>
{data?.totalDocs > 0 && (
<div className={`${baseClass}__page-info`}>
{data.page}
-
{data.totalPages > 1 ? data.limit : data.totalDocs}
{' '}
of
{' '}
{data.totalDocs}
</div>
)}
</div>
</MinimalTemplate>
)}
</Modal>
)}
</Fragment>
);
};
export default UploadButton;

View File

@@ -0,0 +1,45 @@
@import '../../../../../../../scss/styles.scss';
.rich-text-upload {
max-width: base(15);
display: flex;
align-items: center;
background: $color-background-gray;
position: relative;
&__button {
margin: 0;
position: absolute;
top: base(.5);
right: base(.5);
}
h5 {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&__thumbnail {
width: base(4);
max-height: base(4);
img, svg {
object-fit: cover;
width: 100%;
height: 100%;
}
}
&__wrap {
padding: base(.5) base(.5) base(.5) base(1);
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
&--selected {
box-shadow: $focus-box-shadow;
outline: none;
}
}

View File

@@ -0,0 +1,236 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Modal, useModal } from '@faceless-ui/modal';
import { Transforms } from 'slate';
import { ReactEditor, useSlateStatic, useFocused, useSelected } from 'slate-react';
import { useConfig } from '@payloadcms/config-provider';
import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
import FileGraphic from '../../../../../../graphics/File';
import useThumbnail from '../../../../../../../hooks/useThumbnail';
import MinimalTemplate from '../../../../../../templates/Minimal';
import UploadGallery from '../../../../../../elements/UploadGallery';
import ListControls from '../../../../../../elements/ListControls';
import Button from '../../../../../../elements/Button';
import ReactSelect from '../../../../../../elements/ReactSelect';
import Paginator from '../../../../../../elements/Paginator';
import formatFields from '../../../../../../views/collections/List/formatFields';
import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types';
import Label from '../../../../../Label';
import './index.scss';
import '../modal.scss';
const baseClass = 'rich-text-upload';
const baseModalClass = 'rich-text-upload-modal';
const initialParams = {
depth: 0,
};
const Element = ({ attributes, children, element, path }) => {
const { relationTo, value } = element;
const { closeAll, currentModal, open } = useModal();
const { collections, serverURL, routes: { api } } = useConfig();
const [availableCollections] = useState(() => collections.filter(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
const [renderModal, setRenderModal] = useState(false);
const [relatedCollection, setRelatedCollection] = useState<SanitizedCollectionConfig>(() => collections.find((coll) => coll.slug === relationTo));
const [modalCollectionOption, setModalCollectionOption] = useState<{ label: string, value: string}>({ label: relatedCollection.labels.singular, value: relatedCollection.slug });
const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>(relatedCollection);
const [listControls, setListControls] = useState<{where?: unknown}>({});
const [page, setPage] = useState(null);
const [sort, setSort] = useState(null);
const [fields, setFields] = useState(formatFields(relatedCollection));
const editor = useSlateStatic();
const selected = useSelected();
const focused = useFocused();
const modalSlug = `${path}-edit-upload`;
const isOpen = currentModal === modalSlug;
const moreThanOneAvailableCollection = availableCollections.length > 1;
// Get the referenced document
const [{ data: upload }] = usePayloadAPI(
`${serverURL}${api}/${relatedCollection.slug}/${value?.id}`,
{ initialParams },
);
// If modal is open, get active page of upload gallery
const apiURL = isOpen ? `${serverURL}${api}/${modalCollection.slug}` : null;
const [{ data }, { setParams }] = usePayloadAPI(apiURL, {});
const thumbnailSRC = useThumbnail(relatedCollection, upload);
const handleUpdateUpload = useCallback((doc) => {
const newNode = {
type: 'upload',
value: { id: doc.id },
relationTo: modalCollection.slug,
children: [
{ text: ' ' },
],
};
const elementPath = ReactEditor.findPath(editor, element);
Transforms.setNodes(
editor,
newNode,
{ at: elementPath },
);
closeAll();
}, [closeAll, editor, element, modalCollection]);
useEffect(() => {
setFields(formatFields(relatedCollection));
}, [relatedCollection]);
useEffect(() => {
if (renderModal && modalSlug) {
open(modalSlug);
}
}, [renderModal, open, modalSlug]);
useEffect(() => {
const params: {
page?: number
sort?: string
where?: unknown
} = {};
if (page) params.page = page;
if (listControls?.where) params.where = listControls.where;
if (sort) params.sort = sort;
setParams(params);
}, [setParams, page, listControls, sort]);
useEffect(() => {
setModalCollection(collections.find(({ slug }) => modalCollectionOption.value === slug));
}, [modalCollectionOption, collections]);
return (
<div
className={[
baseClass,
(selected && focused) && `${baseClass}--selected`,
].filter(Boolean).join(' ')}
contentEditable={false}
{...attributes}
>
<div className={`${baseClass}__thumbnail`}>
{thumbnailSRC && (
<img
src={thumbnailSRC}
alt={upload?.filename}
/>
)}
{!thumbnailSRC && (
<FileGraphic />
)}
</div>
<div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__label`}>
{relatedCollection.labels.singular}
</div>
<h5>{upload?.filename}</h5>
</div>
<Button
icon="edit"
round
buttonStyle="icon-label"
iconStyle="with-border"
className={`${baseClass}__button`}
onClick={(e) => {
e.preventDefault();
setRenderModal(true);
}}
/>
{children}
{renderModal && (
<Modal
className={baseModalClass}
slug={modalSlug}
>
{isOpen && (
<MinimalTemplate width="wide">
<header className={`${baseModalClass}__header`}>
<h1>
Choose
{' '}
{modalCollection.labels.singular}
</h1>
<Button
icon="x"
round
buttonStyle="icon-label"
iconStyle="with-border"
onClick={() => {
closeAll();
setRenderModal(false);
}}
/>
</header>
{moreThanOneAvailableCollection && (
<div className={`${baseModalClass}__select-collection-wrap`}>
<Label label="Select a Collection to Browse" />
<ReactSelect
className={`${baseClass}__select-collection`}
value={modalCollectionOption}
onChange={setModalCollectionOption}
options={availableCollections.map((coll) => ({ label: coll.labels.singular, value: coll.slug }))}
/>
</div>
)}
<ListControls
handleChange={setListControls}
collection={{
...modalCollection,
fields,
}}
enableColumns={false}
setSort={setSort}
enableSort
/>
<UploadGallery
docs={data?.docs}
collection={modalCollection}
onCardClick={(doc) => {
handleUpdateUpload(doc);
setRelatedCollection(modalCollection);
setRenderModal(false);
closeAll();
}}
/>
<div className={`${baseClass}__page-controls`}>
<Paginator
limit={data.limit}
totalPages={data.totalPages}
page={data.page}
hasPrevPage={data.hasPrevPage}
hasNextPage={data.hasNextPage}
prevPage={data.prevPage}
nextPage={data.nextPage}
numberOfNeighbors={1}
onChange={setPage}
disableHistoryChange
/>
{data?.totalDocs > 0 && (
<div className={`${baseClass}__page-info`}>
{data.page}
-
{data.totalPages > 1 ? data.limit : data.totalDocs}
{' '}
of
{' '}
{data.totalDocs}
</div>
)}
</div>
</MinimalTemplate>
)}
</Modal>
)}
</div>
);
};
export default Element;

View File

@@ -0,0 +1,11 @@
import plugin from './plugin';
import Element from './Element';
import Button from './Button';
export default {
Button,
Element,
plugins: [
plugin,
],
};

View File

@@ -0,0 +1,29 @@
@import '../../../../../../scss/styles.scss';
.rich-text-upload-modal {
@include blur-bg;
display: flex;
align-items: center;
.template-minimal {
padding-top: base(4);
align-items: flex-start;
}
&__header {
margin-bottom: $baseline;
display: flex;
h1 {
margin: 0 auto 0 0;
}
.btn {
margin: 0 0 0 $baseline;
}
}
&__select-collection-wrap {
margin-bottom: base(1);
}
}

View File

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

View File

@@ -4,6 +4,7 @@ import ReactSelect from '../../../elements/ReactSelect';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
import { select } from '../../../../../fields/validations';
import { Option } from '../../../../../fields/config/types';
import { Props, Option as ReactSelectOption } from './types';
@@ -36,6 +37,7 @@ const Select: React.FC<Props> = (props) => {
readOnly,
style,
width,
description,
condition,
} = {},
} = props;
@@ -110,6 +112,10 @@ const Select: React.FC<Props> = (props) => {
options={options}
isMulti={hasMany}
/>
<FieldDescription
value={value}
description={description}
/>
</div>
);
};

View File

@@ -5,6 +5,7 @@ import Label from '../../Label';
import Error from '../../Error';
import { text } from '../../../../../fields/validations';
import { Props } from './types';
import FieldDescription from '../../FieldDescription';
import './index.scss';
@@ -20,6 +21,7 @@ const Text: React.FC<Props> = (props) => {
readOnly,
style,
width,
description,
condition,
} = {},
} = props;
@@ -29,8 +31,8 @@ const Text: React.FC<Props> = (props) => {
const fieldType = useFieldType<string>({
path,
validate,
condition,
enableDebouncedValue: true,
condition,
});
const {
@@ -73,6 +75,10 @@ const Text: React.FC<Props> = (props) => {
id={path}
name={path}
/>
<FieldDescription
value={value}
description={description}
/>
</div>
);
};

View File

@@ -3,6 +3,7 @@ import useFieldType from '../../useFieldType';
import withCondition from '../../withCondition';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
import { textarea } from '../../../../../fields/validations';
import { Props } from './types';
@@ -20,6 +21,7 @@ const Textarea: React.FC<Props> = (props) => {
width,
placeholder,
rows,
description,
condition,
} = {},
label,
@@ -79,6 +81,10 @@ const Textarea: React.FC<Props> = (props) => {
name={path}
rows={rows}
/>
<FieldDescription
value={value}
description={description}
/>
</div>
);
};

View File

@@ -11,9 +11,12 @@
}
&__header {
display: flex;
margin-bottom: $baseline;
div {
display: flex;
}
h1 {
margin: 0 auto 0 0;
}
@@ -22,4 +25,8 @@
margin: 0 0 0 $baseline;
}
}
&__sub-header {
margin-top: base(.25);
}
}

View File

@@ -17,6 +17,11 @@ const baseClass = 'add-upload-modal';
const AddUploadModal: React.FC<Props> = (props) => {
const {
collection,
collection: {
admin: {
description,
} = {},
} = {},
slug,
fieldTypes,
setValue,
@@ -50,19 +55,24 @@ const AddUploadModal: React.FC<Props> = (props) => {
disableSuccessStatus
>
<header className={`${baseClass}__header`}>
<h1>
New
{' '}
{collection.labels.singular}
</h1>
<FormSubmit>Save</FormSubmit>
<Button
icon="x"
round
buttonStyle="icon-label"
iconStyle="with-border"
onClick={closeAll}
/>
<div>
<h1>
New
{' '}
{collection.labels.singular}
</h1>
<FormSubmit>Save</FormSubmit>
<Button
icon="x"
round
buttonStyle="icon-label"
iconStyle="with-border"
onClick={closeAll}
/>
</div>
{description && (
<div className={`${baseClass}__sub-header`}>{description}</div>
)}
</header>
<Upload
collection={collection}

View File

@@ -1,9 +1,9 @@
import { CollectionConfig } from '../../../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
import { FieldTypes } from '../..';
export type Props = {
setValue: (val: { id: string } | null) => void
collection: CollectionConfig
collection: SanitizedCollectionConfig
slug: string
fieldTypes: FieldTypes
}

View File

@@ -11,9 +11,12 @@
}
&__header {
display: flex;
margin-bottom: $baseline;
div {
display: flex;
}
h1 {
margin: 0 auto 0 0;
}
@@ -22,4 +25,8 @@
margin: 0 0 0 $baseline;
}
}
&__sub-header {
margin-top: base(.25);
}
}

View File

@@ -8,6 +8,7 @@ import usePayloadAPI from '../../../../../hooks/usePayloadAPI';
import ListControls from '../../../../elements/ListControls';
import Paginator from '../../../../elements/Paginator';
import UploadGallery from '../../../../elements/UploadGallery';
import { Field } from '../../../../../../fields/config/types';
import { Props } from './types';
import './index.scss';
@@ -20,6 +21,9 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
collection,
collection: {
slug: collectionSlug,
admin: {
description,
} = {},
} = {},
slug: modalSlug,
} = props;
@@ -42,7 +46,7 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
const [{ data }, { setParams }] = usePayloadAPI(apiURL, {});
useEffect(() => {
setFields(formatFields(collection));
setFields(formatFields(collection) as Field[]);
}, [collection]);
useEffect(() => {
@@ -68,19 +72,24 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
{isOpen && (
<MinimalTemplate width="wide">
<header className={`${baseClass}__header`}>
<h1>
{' '}
Select existing
{' '}
{collection.labels.singular}
</h1>
<Button
icon="x"
round
buttonStyle="icon-label"
iconStyle="with-border"
onClick={closeAll}
/>
<div>
<h1>
{' '}
Select existing
{' '}
{collection.labels.singular}
</h1>
<Button
icon="x"
round
buttonStyle="icon-label"
iconStyle="with-border"
onClick={closeAll}
/>
</div>
{description && (
<div className={`${baseClass}__sub-header`}>{description}</div>
)}
</header>
<ListControls
handleChange={setListControls}

View File

@@ -1,7 +1,7 @@
import { CollectionConfig } from '../../../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
export type Props = {
setValue: (val: { id: string } | null) => void
collection: CollectionConfig
collection: SanitizedCollectionConfig
slug: string
}

View File

@@ -13,6 +13,7 @@ import SelectExistingModal from './SelectExisting';
import { Props } from './types';
import './index.scss';
import FieldDescription from '../../FieldDescription';
const baseClass = 'upload';
@@ -30,6 +31,7 @@ const Upload: React.FC<Props> = (props) => {
readOnly,
style,
width,
description,
condition,
} = {},
label,
@@ -161,6 +163,10 @@ const Upload: React.FC<Props> = (props) => {
addModalSlug,
}}
/>
<FieldDescription
value={value}
description={description}
/>
</React.Fragment>
)}
</div>

View File

@@ -13,6 +13,7 @@ import number from './Number';
import checkbox from './Checkbox';
import richText from './RichText';
import radio from './RadioGroup';
import point from './Point';
import blocks from './Blocks';
import group from './Group';
@@ -32,6 +33,7 @@ export type FieldTypes = {
textarea: React.ComponentType
select: React.ComponentType
number: React.ComponentType
point: React.ComponentType
checkbox: React.ComponentType
richText: React.ComponentType
radio: React.ComponentType
@@ -56,6 +58,7 @@ const fieldTypes: FieldTypes = {
number,
checkbox,
richText,
point,
radio,
blocks,
group,

View File

@@ -1,4 +1,4 @@
import { Validate, Condition } from '../../../../fields/config/types';
import { Condition, Validate } from '../../../../fields/config/types';
export type Options = {
path: string

View File

@@ -34,20 +34,24 @@ const withCondition = <P extends Record<string, unknown>>(Field: React.Component
const data = getData();
const siblingData = getSiblingData(path);
const passesCondition = condition ? condition(data, siblingData) : true;
const hasCondition = Boolean(condition);
const currentlyPassesCondition = hasCondition ? condition(data, siblingData) : true;
const field = getField(path);
const existingConditionPasses = field?.passesCondition;
useEffect(() => {
if (!passesCondition) {
const field = getField(path);
dispatchFields({
...field,
path,
valid: true,
});
}
}, [passesCondition, getField, dispatchFields, path]);
if (hasCondition) {
if (!existingConditionPasses && currentlyPassesCondition) {
dispatchFields({ type: 'MODIFY_CONDITION', path, result: true });
}
if (passesCondition) {
if (!currentlyPassesCondition && (existingConditionPasses || typeof existingConditionPasses === 'undefined')) {
dispatchFields({ type: 'MODIFY_CONDITION', path, result: false });
}
}
}, [currentlyPassesCondition, existingConditionPasses, dispatchFields, path, hasCondition]);
if (currentlyPassesCondition) {
return <Field {...props} />;
}

View File

@@ -0,0 +1,12 @@
@import '../../../scss/styles';
.icon--edit {
height: $baseline;
width: $baseline;
shape-rendering: auto;
.fill {
fill: $color-dark-gray;
stroke: none;
}
}

View File

@@ -0,0 +1,30 @@
import React from 'react';
import './index.scss';
const Edit: React.FC = () => (
<svg
className="icon icon--edit"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 25 25"
>
<polygon
className="fill"
points="16.92 16.86 8.25 16.86 8.25 8.21 12.54 8.21 12.54 6.63 6.68 6.63 6.68 18.43 18.5 18.43 18.5 12.53 16.92 12.53 16.92 16.86"
/>
<polygon
className="fill"
points="16.31 7.33 17.42 8.44 12.66 13.2 11.51 13.24 11.55 12.09 16.31 7.33"
/>
<rect
className="fill"
x="16.94"
y="6.44"
width="1.58"
height="1.15"
transform="translate(10.16 -10.48) rotate(45)"
/>
</svg>
);
export default Edit;

View File

@@ -0,0 +1,11 @@
@import '../../../scss/styles';
.icon--upload {
height: $baseline;
width: $baseline;
.fill {
fill: $color-dark-gray;
stroke: none;
}
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import './index.scss';
const Upload: React.FC = () => (
<svg
className="icon icon--upload"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 25 25"
>
<path
d="M20.06,5.12h-15v15h15Zm-2,2v7L15.37,11l-3.27,4.1-2-1.58-3,3.74V7.12Z"
className="fill"
/>
<circle
cx="9.69"
cy="9.47"
r="0.97"
className="fill"
/>
</svg>
);
export default Upload;

View File

@@ -9,3 +9,4 @@ export { default as Chevron } from './icons/Chevron';
export { default as Check } from './icons/Check';
export { default as Search } from './icons/Search';
export { default as Menu } from './icons/Menu';
export { default as Pill } from './elements/Pill';

View File

@@ -1,11 +1,11 @@
import { CollectionConfig } from '../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
import { Fields, Data } from '../../forms/Form/types';
import { CollectionPermission } from '../../../../auth/types';
export type Props = {
hasSavePermission: boolean
apiURL: string
collection: CollectionConfig
collection: SanitizedCollectionConfig
data: Data
permissions: CollectionPermission
initialState: Fields

View File

@@ -1,9 +1,9 @@
import { CollectionConfig } from '../../../../collections/config/types';
import { GlobalConfig } from '../../../../globals/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
import { SanitizedGlobalConfig } from '../../../../globals/config/types';
import { Permissions } from '../../../../auth/types';
export type Props = {
collections: CollectionConfig[],
globals: GlobalConfig[],
collections: SanitizedCollectionConfig[],
globals: SanitizedGlobalConfig[],
permissions: Permissions
}

View File

@@ -13,6 +13,7 @@ import LeaveWithoutSaving from '../../modals/LeaveWithoutSaving';
import { Props } from './types';
import './index.scss';
import ViewDescription from '../../elements/ViewDescription';
const baseClass = 'global-edit';
@@ -26,6 +27,9 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
fields,
preview,
label,
admin: {
description,
} = {},
} = global;
const hasSavePermission = permissions?.update?.permission;
@@ -55,6 +59,11 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
{' '}
{label}
</h1>
{description && (
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={description} />
</div>
)}
</header>
<RenderFields
operation="update"

View File

@@ -19,7 +19,13 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
}
margin-bottom: base(1);
}
&__sub-header {
margin-top: base(.25);
}
&__collection-actions {

View File

@@ -1,13 +1,13 @@
import { GlobalPermission } from '../../../../auth/types';
import { GlobalConfig } from '../../../../globals/config/types';
import { SanitizedGlobalConfig } from '../../../../globals/config/types';
import { Fields } from '../../forms/Form/types';
export type IndexProps = {
global: GlobalConfig
global: SanitizedGlobalConfig
}
export type Props = {
global: GlobalConfig
global: SanitizedGlobalConfig
data: Record<string, unknown>
onSave: () => void
permissions: GlobalPermission

View File

@@ -5,14 +5,14 @@ import Logo from '../../graphics/Logo';
import MinimalTemplate from '../../templates/Minimal';
import Button from '../../elements/Button';
import Meta from '../../utilities/Meta';
import { CollectionConfig } from '../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
import Login from '../Login';
import './index.scss';
const baseClass = 'verify';
const Verify: React.FC<{ collection: CollectionConfig }> = ({ collection }) => {
const Verify: React.FC<{ collection: SanitizedCollectionConfig }> = ({ collection }) => {
const { slug: collectionSlug } = collection;
const { user } = useAuth();

View File

@@ -1,11 +1,11 @@
import { CollectionConfig } from '../../../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
import { VerifyConfig } from '../../../../../../auth/types';
export type Props = {
useAPIKey?: boolean
requirePassword?: boolean
verify?: VerifyConfig | boolean
collection: CollectionConfig
collection: SanitizedCollectionConfig
email: string
operation: 'update' | 'create'
}

View File

@@ -1,4 +1,4 @@
import { CollectionConfig } from '../../../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
export type Data = {
filename: string
@@ -8,7 +8,7 @@ export type Data = {
export type Props = {
data?: Data
collection: CollectionConfig
collection: SanitizedCollectionConfig
adminThumbnail?: string
mimeTypes?: string[];
}

View File

@@ -0,0 +1,8 @@
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { Field } from '../../../../../fields/config/types';
const formatFields = (collection: SanitizedCollectionConfig, isEditing: boolean): Field[] => (isEditing
? collection.fields.filter(({ name }) => name !== 'id')
: collection.fields);
export default formatFields;

View File

@@ -7,6 +7,7 @@ import usePayloadAPI from '../../../../hooks/usePayloadAPI';
import RenderCustomComponent from '../../../utilities/RenderCustomComponent';
import { DocumentInfoProvider } from '../../../utilities/DocumentInfo';
import DefaultEdit from './Default';
import formatFields from './formatFields';
import buildStateFromSchema from '../../../forms/Form/buildStateFromSchema';
import { NegativeFieldGutterProvider } from '../../../forms/FieldTypeGutter/context';
import { useLocale } from '../../../utilities/Locale';
@@ -29,8 +30,8 @@ const EditView: React.FC<IndexProps> = (props) => {
} = {},
} = {},
} = {},
fields,
} = collection;
const [fields] = useState(() => formatFields(collection, isEditing));
const locale = useLocale();
const { serverURL, routes: { admin, api } } = useConfig();
@@ -110,7 +111,7 @@ const EditView: React.FC<IndexProps> = (props) => {
componentProps={{
isLoading,
data: dataToRender,
collection,
collection: { ...collection, fields },
permissions: collectionPermissions,
isEditing,
onSave,

View File

@@ -1,10 +1,10 @@
import { CollectionConfig } from '../../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { CollectionPermission } from '../../../../../auth/types';
import { Document } from '../../../../../types';
import { Fields } from '../../../forms/Form/types';
export type IndexProps = {
collection: CollectionConfig
collection: SanitizedCollectionConfig
isEditing?: boolean
}

View File

@@ -1,10 +1,10 @@
import { Field } from '../../../../../../fields/config/types';
import { CollectionConfig } from '../../../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
export type Props = {
field: Field
colIndex: number
collection: CollectionConfig
collection: SanitizedCollectionConfig
cellData: unknown
rowData: {
[path: string]: unknown

View File

@@ -12,6 +12,7 @@ import Meta from '../../../utilities/Meta';
import { Props } from './types';
import './index.scss';
import ViewDescription from '../../../elements/ViewDescription';
const baseClass = 'collection-list';
@@ -25,6 +26,9 @@ const DefaultList: React.FC<Props> = (props) => {
singular: singularLabel,
plural: pluralLabel,
},
admin: {
description,
} = {},
},
data,
newDocumentURL,
@@ -52,6 +56,11 @@ const DefaultList: React.FC<Props> = (props) => {
Create New
</Pill>
)}
{description && (
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={description} />
</div>
)}
</header>
<ListControls
handleChange={setListControls}

View File

@@ -1,11 +1,11 @@
import React from 'react';
import Cell from './Cell';
import SortColumn from '../../../elements/SortColumn';
import { CollectionConfig } from '../../../../../collections/config/types';
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { Column } from '../../../elements/Table/types';
import { fieldHasSubFields, Field } from '../../../../../fields/config/types';
const buildColumns = (collection: CollectionConfig, columns: string[], setSort: (sort: string) => void): Column[] => (columns || []).reduce((cols, col, colIndex) => {
const buildColumns = (collection: SanitizedCollectionConfig, columns: string[], setSort: (sort: string) => void): Column[] => (columns || []).reduce((cols, col, colIndex) => {
let field = null;
const fields = [

View File

@@ -1,5 +1,9 @@
const formatFields = (config) => {
let fields = config.fields.reduce((formatted, field) => {
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { Field } from '../../../../../fields/config/types';
const formatFields = (config: SanitizedCollectionConfig): Field[] => {
const hasID = config.fields.findIndex(({ name }) => name === 'id') > -1;
let fields: Field[] = config.fields.reduce((formatted, field) => {
if (field.hidden === true || field?.admin?.disabled === true) {
return formatted;
}
@@ -8,7 +12,7 @@ const formatFields = (config) => {
...formatted,
field,
];
}, [{ name: 'id', label: 'ID', type: 'text' }]);
}, hasID ? [] : [{ name: 'id', label: 'ID', type: 'text' }]);
if (config.timestamps) {
fields = fields.concat([

View File

@@ -29,6 +29,11 @@
}
}
&__sub-header {
flex-basis: 100%;
margin-top: base(.25);
}
.table {
table {
width: 100%;

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