Merge branch 'master' of github.com:payloadcms/payload into fix/relationship-access-missing-id
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type Props = {
|
||||
className?: string
|
||||
render?: (any) => void,
|
||||
children?: React.ReactNode,
|
||||
horizontalAlign?: 'left' | 'center' | 'right',
|
||||
|
||||
@@ -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(' ');
|
||||
|
||||
@@ -7,6 +7,7 @@ export type Value = {
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
className?: string
|
||||
value?: Value | Value[],
|
||||
onChange?: (value: any) => void,
|
||||
disabled?: boolean,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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(' ');
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
28
src/admin/components/elements/ViewDescription/index.tsx
Normal file
28
src/admin/components/elements/ViewDescription/index.tsx
Normal 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;
|
||||
15
src/admin/components/elements/ViewDescription/types.ts
Normal file
15
src/admin/components/elements/ViewDescription/types.ts
Normal 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);
|
||||
}
|
||||
@@ -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],
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
8
src/admin/components/forms/FieldDescription/index.scss
Normal file
8
src/admin/components/forms/FieldDescription/index.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.field-description {
|
||||
display: flex;
|
||||
padding-top: base(.25);
|
||||
padding-bottom: base(.25);
|
||||
color: $color-gray;
|
||||
}
|
||||
30
src/admin/components/forms/FieldDescription/index.tsx
Normal file
30
src/admin/components/forms/FieldDescription/index.tsx
Normal 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;
|
||||
16
src/admin/components/forms/FieldDescription/types.ts
Normal file
16
src/admin/components/forms/FieldDescription/types.ts
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,6 +10,7 @@ export type Field = {
|
||||
ignoreWhileFlattening?: boolean
|
||||
stringify?: boolean
|
||||
condition?: Condition
|
||||
passesCondition?: boolean
|
||||
}
|
||||
|
||||
export type Fields = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
label.field-label {
|
||||
display: flex;
|
||||
padding-bottom: base(.25);
|
||||
color: $color-gray;
|
||||
color: $color-dark-gray;
|
||||
|
||||
.required {
|
||||
color: $color-red;
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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%);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
30
src/admin/components/forms/field-types/Point/index.scss
Normal file
30
src/admin/components/forms/field-types/Point/index.scss
Normal 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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
src/admin/components/forms/field-types/Point/index.tsx
Normal file
124
src/admin/components/forms/field-types/Point/index.tsx
Normal 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);
|
||||
5
src/admin/components/forms/field-types/Point/types.ts
Normal file
5
src/admin/components/forms/field-types/Point/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { NumberField } from '../../../../../fields/config/types';
|
||||
|
||||
export type Props = Omit<NumberField, 'type'> & {
|
||||
path?: string
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,4 +19,9 @@
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
box-shadow: $focus-box-shadow;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../../../../../../../scss/styles.scss';
|
||||
|
||||
.upload-rich-text-button {
|
||||
.btn {
|
||||
margin-right: base(1);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,11 @@
|
||||
import plugin from './plugin';
|
||||
import Element from './Element';
|
||||
import Button from './Button';
|
||||
|
||||
export default {
|
||||
Button,
|
||||
Element,
|
||||
plugins: [
|
||||
plugin,
|
||||
],
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Validate, Condition } from '../../../../fields/config/types';
|
||||
import { Condition, Validate } from '../../../../fields/config/types';
|
||||
|
||||
export type Options = {
|
||||
path: string
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
|
||||
12
src/admin/components/icons/Edit/index.scss
Normal file
12
src/admin/components/icons/Edit/index.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
.icon--edit {
|
||||
height: $baseline;
|
||||
width: $baseline;
|
||||
shape-rendering: auto;
|
||||
|
||||
.fill {
|
||||
fill: $color-dark-gray;
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
30
src/admin/components/icons/Edit/index.tsx
Normal file
30
src/admin/components/icons/Edit/index.tsx
Normal 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;
|
||||
11
src/admin/components/icons/Upload/index.scss
Normal file
11
src/admin/components/icons/Upload/index.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
.icon--upload {
|
||||
height: $baseline;
|
||||
width: $baseline;
|
||||
|
||||
.fill {
|
||||
fill: $color-dark-gray;
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
24
src/admin/components/icons/Upload/index.tsx
Normal file
24
src/admin/components/icons/Upload/index.tsx
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user