feat: adds collapsible field type

This commit is contained in:
James
2022-07-13 14:45:10 -07:00
parent 8589fdefda
commit 98676bea69
23 changed files with 421 additions and 44 deletions

View File

@@ -0,0 +1,49 @@
@import '../../../scss/styles.scss';
.collapsible {
border: 1px solid var(--theme-elevation-200);
border-radius: $style-radius-m;
&:hover {
border: 1px solid var(--theme-elevation-300);
}
header {
position: relative;
background: var(--theme-elevation-100);
border-top-right-radius: $style-radius-s;
border-top-left-radius: $style-radius-s;
display: flex;
padding: base(.75) base(1);
}
&--collapsed {
header {
border-bottom-right-radius: $style-radius-s;
border-bottom-left-radius: $style-radius-s;
}
}
&__toggle {
margin: 0 0 0 auto;
transform: rotate(.5turn);
.btn__icon {
background-color: var(--theme-elevation-0);
}
&--collapsed {
transform: rotate(0turn);
}
}
&__content {
padding: $baseline;
}
@include small-break {
header {
padding: base(.75) var(--gutter-h);
}
}
}

View File

@@ -1,14 +1,52 @@
import React from 'react';
import React, { useState } from 'react';
import AnimateHeight from 'react-animate-height';
import Button from '../Button';
import { Props } from './types';
import { CollapsibleProvider, useCollapsible } from './provider';
import './index.scss';
const baseClass = 'collapsible';
export const Collapsible: React.FC<Props> = ({ children }) => {
export const Collapsible: React.FC<Props> = ({ children, onToggle, className, header }) => {
const [collapsed, setCollapsed] = useState(false);
const isNested = useCollapsible();
return (
<div className={baseClass}>
<div className={[
baseClass,
className,
collapsed && `${baseClass}--collapsed`,
isNested && `${baseClass}--nested`,
].filter(Boolean).join(' ')}
>
<CollapsibleProvider withinCollapsible>
<header>
{header && (
<div className={`${baseClass}__header-wrap`}>
{header}
</div>
)}
<Button
icon="chevron"
onClick={() => {
if (typeof onToggle === 'function') onToggle(!collapsed);
setCollapsed(!collapsed);
}}
buttonStyle="icon-label"
className={`${baseClass}__toggle ${baseClass}__toggle--${collapsed ? 'collapsed' : 'open'}`}
round
/>
</header>
<AnimateHeight
height={collapsed ? 0 : 'auto'}
duration={200}
>
<div className={`${baseClass}__content`}>
{children}
</div>
</AnimateHeight>
</CollapsibleProvider>
</div>
);
};

View File

@@ -0,0 +1,17 @@
import React, {
createContext, useContext,
} from 'react';
const Context = createContext(false);
export const CollapsibleProvider: React.FC<{ children?: React.ReactNode, withinCollapsible: boolean }> = ({ children, withinCollapsible }) => {
return (
<Context.Provider value={withinCollapsible}>
{children}
</Context.Provider>
);
};
export const useCollapsible = (): boolean => useContext(Context);
export default Context;

View File

@@ -1,5 +1,8 @@
import React from 'react';
export type Props = {
className?: string
header?: React.ReactNode
children: React.ReactNode
onToggle?: (collapsed: boolean) => void
}

View File

@@ -26,19 +26,6 @@
margin-left: - base(.75);
margin-right: - base(.75);
width: calc(100% + #{base(1.5)});
.toggle-collapse {
margin: 0 0 0 auto;
transform: rotate(.5turn);
.btn__icon {
background-color: var(--theme-elevation-0);
}
&--is-collapsed {
transform: rotate(0turn);
}
}
}
&__render-fields-wrapper {

View File

@@ -0,0 +1,11 @@
@import '../../../../scss/styles.scss';
.collapsible-field {
&__label {
font-weight: 600;
}
.field-type:last-child {
margin-bottom: 0;
}
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import RenderFields from '../../RenderFields';
import withCondition from '../../withCondition';
import { Props } from './types';
import { fieldAffectsData } from '../../../../../fields/config/types';
import { Collapsible } from '../../../elements/Collapsible';
import './index.scss';
const baseClass = 'collapsible-field';
const CollapsibleField: React.FC<Props> = (props) => {
const {
label,
fields,
fieldTypes,
path,
permissions,
admin: {
readOnly,
className,
},
} = props;
const classes = [
'field-type',
baseClass,
className,
].filter(Boolean).join(' ');
return (
<Collapsible
className={classes}
header={<div className={`${baseClass}__label`}>{label}</div>}
>
<RenderFields
readOnly={readOnly}
permissions={permissions?.fields}
fieldTypes={fieldTypes}
fieldSchema={fields.map((field) => ({
...field,
path: `${path ? `${path}.` : ''}${fieldAffectsData(field) ? field.name : ''}`,
}))}
/>
</Collapsible>
);
};
export default withCondition(CollapsibleField);

View File

@@ -0,0 +1,9 @@
import { CollapsibleField } from '../../../../../fields/config/types';
import { FieldTypes } from '..';
import { FieldPermissions } from '../../../../../auth/types';
export type Props = Omit<CollapsibleField, 'type'> & {
path?: string
fieldTypes: FieldTypes
permissions: FieldPermissions
}

View File

@@ -19,6 +19,7 @@ import blocks from './Blocks';
import group from './Group';
import array from './Array';
import row from './Row';
import collapsible from './Collapsible';
import upload from './Upload';
import ui from './UI';
@@ -42,6 +43,7 @@ export type FieldTypes = {
group: React.ComponentType
array: React.ComponentType
row: React.ComponentType
collapsible: React.ComponentType
upload: React.ComponentType
ui: React.ComponentType
}
@@ -66,6 +68,7 @@ const fieldTypes: FieldTypes = {
group,
array,
row,
collapsible,
upload,
ui,
};

View File

@@ -12,6 +12,7 @@ export default {
checkbox: Text,
radio: Text,
row: Nested,
collapsible: Nested,
group: Nested,
array: Iterable,
blocks: Iterable,

View File

@@ -133,7 +133,7 @@
}
@include small-break {
--gutter-h: #{base(1)};
--gutter-h: #{base(.75)};
}
}

View File

@@ -175,6 +175,16 @@ export const row = baseField.keys({
}),
});
export const collapsible = baseField.keys({
label: joi.string().required(),
type: joi.string().valid('collapsible').required(),
fields: joi.array().items(joi.link('#field')),
admin: baseAdminFields.keys({
readOnly: joi.forbidden(),
hidden: joi.forbidden(),
}),
});
export const group = baseField.keys({
type: joi.string().valid('group').required(),
name: joi.string().required(),
@@ -367,6 +377,7 @@ const fieldSchema = joi.alternatives()
group,
array,
row,
collapsible,
radio,
relationship,
checkbox,

View File

@@ -165,10 +165,7 @@ export type GroupField = FieldBase & {
}
}
export type RowAdmin = Omit<Admin, 'description'> & {
readOnly?: false;
hidden?: false;
};
export type RowAdmin = Omit<Admin, 'description'>;
export type RowField = Omit<FieldBase, 'admin' | 'name'> & {
admin?: RowAdmin;
@@ -176,6 +173,12 @@ export type RowField = Omit<FieldBase, 'admin' | 'name'> & {
fields: Field[];
}
export type CollapsibleField = Omit<FieldBase, 'name'> & {
type: 'collapsible';
label: string
fields: Field[];
}
export type UIField = {
name: string
label?: string
@@ -328,6 +331,7 @@ export type Field =
| CodeField
| PointField
| RowField
| CollapsibleField
| UIField;
export type FieldAffectingData =
@@ -364,7 +368,8 @@ export type NonPresentationalField = TextField
| UploadField
| CodeField
| PointField
| RowField;
| RowField
| CollapsibleField;
export type FieldWithPath = Field & {
path?: string
@@ -373,7 +378,8 @@ export type FieldWithPath = Field & {
export type FieldWithSubFields =
GroupField
| ArrayField
| RowField;
| RowField
| CollapsibleField;
export type FieldPresentationalOnly =
UIField;
@@ -387,7 +393,7 @@ export type FieldWithMaxDepth =
| RelationshipField
export function fieldHasSubFields(field: Field): field is FieldWithSubFields {
return (field.type === 'group' || field.type === 'array' || field.type === 'row');
return (field.type === 'group' || field.type === 'array' || field.type === 'row' || field.type === 'collapsible');
}
export function fieldIsArrayType(field: Field): field is ArrayField {

View File

@@ -111,7 +111,8 @@ export const promise = async ({
break;
}
case 'row': {
case 'row':
case 'collapsible': {
traverseFields({
data,
doc,

View File

@@ -246,7 +246,8 @@ export const promise = async ({
break;
}
case 'row': {
case 'row':
case 'collapsible': {
traverseFields({
currentDepth,
depth,

View File

@@ -259,7 +259,8 @@ export const promise = async ({
break;
}
case 'row': {
case 'row':
case 'collapsible': {
traverseFields({
data,
doc,

View File

@@ -249,7 +249,8 @@ export const promise = async ({
break;
}
case 'row': {
case 'row':
case 'collapsible': {
traverseFields({
data,
doc,

View File

@@ -15,7 +15,7 @@ import { GraphQLJSON } from 'graphql-type-json';
import withNullableType from './withNullableType';
import formatName from '../utilities/formatName';
import combineParentName from '../utilities/combineParentName';
import { ArrayField, CodeField, DateField, EmailField, Field, fieldHasSubFields, fieldAffectsData, fieldIsPresentationalOnly, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField } from '../../fields/config/types';
import { ArrayField, CodeField, DateField, EmailField, Field, fieldHasSubFields, fieldAffectsData, fieldIsPresentationalOnly, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, CollapsibleField } from '../../fields/config/types';
import { toWords } from '../../utilities/formatLabels';
import { Payload } from '../../index';
import { SanitizedCollectionConfig } from '../../collections/config/types';
@@ -135,6 +135,20 @@ function buildMutationInputType(payload: Payload, name: string, fields: Field[],
];
}
return acc;
}, []),
collapsible: (field: CollapsibleField) => field.fields.reduce((acc, collapsibleField: CollapsibleField) => {
const getFieldSchema = fieldToSchemaMap[collapsibleField.type];
if (getFieldSchema) {
const fieldSchema = getFieldSchema(collapsibleField);
return [
...acc,
fieldSchema,
];
}
return acc;
}, []),
};

View File

@@ -429,6 +429,18 @@ function buildObjectType(payload: Payload, name: string, fields: Field[], parent
};
}
return subFieldSchema;
}, {}),
collapsible: (field) => field.fields.reduce((subFieldSchema, subField) => {
const buildSchemaType = fieldToSchemaMap[subField.type];
if (!fieldIsPresentationalOnly(subField) && buildSchemaType) {
return {
...subFieldSchema,
[formatName(subField.name)]: buildSchemaType(subField),
};
}
return subFieldSchema;
}, {}),
};

View File

@@ -11,7 +11,7 @@ import { GraphQLJSON } from 'graphql-type-json';
import {
ArrayField,
CheckboxField,
CodeField, DateField,
CodeField, CollapsibleField, DateField,
EmailField, fieldAffectsData, fieldHasSubFields, GroupField,
NumberField, optionIsObject, PointField,
RadioField, RelationshipField,
@@ -220,24 +220,51 @@ const fieldToSchemaMap: (parentName: string) => any = (parentName: string) => ({
}),
array: (field: ArrayField) => recursivelyBuildNestedPaths(parentName, field),
group: (field: GroupField) => recursivelyBuildNestedPaths(parentName, field),
row: (field: RowField) => field.fields.reduce((rowSchema, rowField) => {
const getFieldSchema = fieldToSchemaMap(parentName)[rowField.type];
row: (field: RowField) => field.fields.reduce((rowSchema, subField) => {
const getFieldSchema = fieldToSchemaMap(parentName)[subField.type];
if (getFieldSchema) {
const rowFieldSchema = getFieldSchema(rowField);
const rowFieldSchema = getFieldSchema(subField);
if (fieldHasSubFields(rowField)) {
if (fieldHasSubFields(subField)) {
return [
...rowSchema,
...rowFieldSchema,
];
}
if (fieldAffectsData(rowField)) {
if (fieldAffectsData(subField)) {
return [
...rowSchema,
{
key: rowField.name,
key: subField.name,
type: rowFieldSchema,
},
];
}
}
return rowSchema;
}, []),
collapsible: (field: CollapsibleField) => field.fields.reduce((rowSchema, subField) => {
const getFieldSchema = fieldToSchemaMap(parentName)[subField.type];
if (getFieldSchema) {
const rowFieldSchema = getFieldSchema(subField);
if (fieldHasSubFields(subField)) {
return [
...rowSchema,
...rowFieldSchema,
];
}
if (fieldAffectsData(subField)) {
return [
...rowSchema,
{
key: subField.name,
type: rowFieldSchema,
},
];

View File

@@ -4,7 +4,7 @@
/* eslint-disable no-use-before-define */
import { Schema, SchemaDefinition, SchemaOptions } from 'mongoose';
import { SanitizedConfig } from '../config/types';
import { ArrayField, Block, BlockField, CheckboxField, CodeField, DateField, EmailField, Field, fieldAffectsData, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, fieldIsPresentationalOnly, NonPresentationalField } from '../fields/config/types';
import { ArrayField, Block, BlockField, CheckboxField, CodeField, DateField, EmailField, Field, fieldAffectsData, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, fieldIsPresentationalOnly, NonPresentationalField, CollapsibleField } from '../fields/config/types';
import sortableFieldTypes from '../fields/sortableFieldTypes';
export type BuildSchemaOptions = {
@@ -306,12 +306,26 @@ const fieldToSchemaMap = {
row: (field: RowField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
const newFields = { ...fields };
field.fields.forEach((rowField: Field) => {
const fieldSchemaMap: FieldSchemaGenerator = fieldToSchemaMap[rowField.type];
field.fields.forEach((subField: Field) => {
const fieldSchemaMap: FieldSchemaGenerator = fieldToSchemaMap[subField.type];
if (fieldSchemaMap && fieldAffectsData(rowField)) {
const fieldSchema = fieldSchemaMap(rowField, fields, config, buildSchemaOptions);
newFields[rowField.name] = fieldSchema[rowField.name];
if (fieldSchemaMap && fieldAffectsData(subField)) {
const fieldSchema = fieldSchemaMap(subField, fields, config, buildSchemaOptions);
newFields[subField.name] = fieldSchema[subField.name];
}
});
return newFields;
},
collapsible: (field: CollapsibleField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
const newFields = { ...fields };
field.fields.forEach((subField: Field) => {
const fieldSchemaMap: FieldSchemaGenerator = fieldToSchemaMap[subField.type];
if (fieldSchemaMap && fieldAffectsData(subField)) {
const fieldSchema = fieldSchemaMap(subField, fields, config, buildSchemaOptions);
newFields[subField.name] = fieldSchema[subField.name];
}
});

View File

@@ -1,9 +1,74 @@
import { buildConfig } from '../buildConfig';
import { devUser } from '../../credentials';
import { textDoc } from './shared';
import { arrayDoc, blocksDoc, collapsibleDoc, textDoc } from './shared';
export default buildConfig({
collections: [
{
slug: 'array-fields',
fields: [
{
name: 'array',
type: 'array',
required: true,
fields: [
{
name: 'text',
type: 'text',
required: true,
},
],
},
],
},
{
slug: 'block-fields',
fields: [
{
name: 'blocks',
type: 'blocks',
required: true,
blocks: [
{
slug: 'text',
fields: [
{
name: 'text',
type: 'text',
required: true,
},
],
},
{
slug: 'number',
fields: [
{
name: 'number',
type: 'number',
required: true,
},
],
},
],
},
],
},
{
slug: 'collapsible-fields',
fields: [
{
label: 'Collapsible Field',
type: 'collapsible',
fields: [
{
name: 'text',
type: 'text',
required: true,
},
],
},
],
},
{
slug: 'text-fields',
admin: {
@@ -27,6 +92,21 @@ export default buildConfig({
},
});
await payload.create({
collection: 'array-fields',
data: arrayDoc,
});
await payload.create({
collection: 'block-fields',
data: blocksDoc,
});
await payload.create({
collection: 'collapsible-fields',
data: collapsibleDoc,
});
await payload.create({
collection: 'text-fields',
data: textDoc,

View File

@@ -1,3 +1,45 @@
export const arrayDoc = {
array: [
{
text: 'first row',
},
{
text: 'second row',
},
{
text: 'third row',
},
{
text: 'fourth row',
},
{
text: 'fifth row',
},
{
text: 'sixth row',
},
],
};
export const blocksDoc = {
blocks: [
{
blockName: 'First block',
blockType: 'text',
text: 'first block',
},
{
blockName: 'Second block',
blockType: 'number',
number: 342,
},
],
};
export const textDoc = {
text: 'Seeded text document',
};
export const collapsibleDoc = {
text: 'Seeded collapsible doc',
};