feat: scaffolds tabs field

This commit is contained in:
James
2022-07-15 16:24:54 -07:00
parent ba79b4446c
commit 85b7a490eb
30 changed files with 487 additions and 27 deletions

View File

@@ -8,4 +8,13 @@
margin-right: base(.5);
margin-bottom: base(.5);
}
&__column {
background-color: transparent;
box-shadow: 0 0 0 1px var(--theme-elevation-200);
}
&__column--active {
background-color: var(--theme-elevation-150);
}
}

View File

@@ -37,8 +37,10 @@ const ColumnSelector: React.FC<Props> = (props) => {
alignIcon="left"
key={field.name || i}
icon={isEnabled ? <X /> : <Plus />}
pillStyle={isEnabled ? 'dark' : undefined}
className={`${baseClass}__active-column`}
className={[
`${baseClass}__column`,
isEnabled && `${baseClass}__column--active`,
].filter(Boolean).join(' ')}
>
{field.label || field.name}
</Pill>

View File

@@ -208,5 +208,21 @@ export const addFieldStatePromise = async ({
locale,
operation,
});
} else if (field.type === 'tabs') {
field.tabs.forEach((tab) => {
iterateFields({
state,
fields: tab.fields,
data,
parentPassesCondition: passesCondition,
path,
user,
fieldPromises,
fullData,
id,
locale,
operation,
});
});
}
};

View File

@@ -284,7 +284,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
forceRender
readOnly={readOnly}
fieldTypes={fieldTypes}
permissions={permissions.fields}
permissions={permissions?.fields}
fieldSchema={fields.map((field) => ({
...field,
path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`,

View File

@@ -1,6 +1,10 @@
@import '../../../../scss/styles.scss';
.collapsible-field {
&__label {
pointer-events: none;
}
.field-type:last-child {
margin-bottom: 0;
}

View File

@@ -0,0 +1,76 @@
import React, { useState } from 'react';
import RenderFields from '../../RenderFields';
import withCondition from '../../withCondition';
import { Props } from './types';
import { fieldAffectsData } from '../../../../../fields/config/types';
import FieldDescription from '../../FieldDescription';
import toKebabCase from '../../../../../utilities/toKebabCase';
import './index.scss';
const baseClass = 'tabs-field';
const TabsField: React.FC<Props> = (props) => {
const {
tabs,
fieldTypes,
path,
permissions,
admin: {
readOnly,
className,
},
} = props;
const [active, setActive] = useState(0);
const activeTab = tabs[active];
return (
<div className={[
className,
baseClass,
].filter(Boolean).join(' ')}
>
<div className={`${baseClass}__tabs`}>
{tabs.map((tab, i) => {
return (
<button
key={i}
type="button"
className={`${baseClass}__tab`}
onClick={() => setActive(i)}
>
{tab.label}
</button>
);
})}
</div>
<div className={`${baseClass}__content-wrap`}>
{activeTab && (
<div className={[
`${baseClass}__tab`,
`${baseClass}__tab-${toKebabCase(activeTab.label)}`,
].join('')}
>
<FieldDescription
description={activeTab.description}
/>
<RenderFields
forceRender
readOnly={readOnly}
permissions={permissions?.fields}
fieldTypes={fieldTypes}
fieldSchema={activeTab.fields.map((field) => ({
...field,
path: `${path ? `${path}.` : ''}${fieldAffectsData(field) ? field.name : ''}`,
}))}
/>
</div>
)}
</div>
</div>
);
};
export default withCondition(TabsField);

View File

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

View File

@@ -20,6 +20,7 @@ import group from './Group';
import array from './Array';
import row from './Row';
import collapsible from './Collapsible';
import tabs from './Tabs';
import upload from './Upload';
import ui from './UI';
@@ -44,6 +45,7 @@ export type FieldTypes = {
array: React.ComponentType
row: React.ComponentType
collapsible: React.ComponentType
tabs: React.ComponentType
upload: React.ComponentType
ui: React.ComponentType
}
@@ -69,6 +71,7 @@ const fieldTypes: FieldTypes = {
array,
row,
collapsible,
tabs,
upload,
ui,
};

View File

@@ -86,6 +86,22 @@ const RenderFieldsToDiff: React.FC<Props> = ({
);
}
if (field.type === 'tabs') {
return field.tabs.map((tab, tabIndex) => {
return (
<RenderFieldsToDiff
key={tabIndex}
fields={tab.fields}
fieldComponents={fieldComponents}
fieldPermissions={fieldPermissions}
version={version}
comparison={comparison}
locales={locales}
/>
);
});
}
// At this point, we are dealing with a `row` or similar
if (fieldHasSubFields(field)) {
return (

View File

@@ -64,6 +64,10 @@ async function accessOperation(args: Arguments): Promise<Permissions> {
}
} else if (field.fields) {
executeFieldPolicies(updatedObj, field.fields, operation);
} else if (field.type === 'tabs') {
field.tabs.forEach((tab) => {
executeFieldPolicies(updatedObj, tab.fields, operation);
});
}
});
};

View File

@@ -269,13 +269,23 @@ function generateFieldTypes(config: SanitizedConfig, fields: Field[]): {
break;
}
case 'row': {
case 'row':
case 'collapsible': {
const topLevelFields = generateFieldTypes(config, field.fields);
requiredTopLevelProps = requiredTopLevelProps.concat(topLevelFields.required);
topLevelProps = topLevelProps.concat(Object.entries(topLevelFields.properties).map((prop) => prop));
break;
}
case 'tabs': {
field.tabs.forEach((tab) => {
const topLevelFields = generateFieldTypes(config, tab.fields);
requiredTopLevelProps = requiredTopLevelProps.concat(topLevelFields.required);
topLevelProps = topLevelProps.concat(Object.entries(topLevelFields.properties).map((prop) => prop));
});
break;
}
case 'group': {
fieldSchema = {
type: 'object',

View File

@@ -135,7 +135,7 @@ function initCollectionsGraphQL(payload: Payload): void {
collection.graphQL.updateMutationInputType = new GraphQLNonNull(buildMutationInputType(
payload,
`${singularLabel}Update`,
fields.filter((field) => fieldAffectsData(field) && field.name !== 'id'),
fields.filter((field) => !(fieldAffectsData(field) && field.name === 'id')),
`${singularLabel}Update`,
true,
));

View File

@@ -68,6 +68,14 @@ const sanitizeFields = (fields: Field[], validRelationships: string[]): Field[]
if ('fields' in field && field.fields) field.fields = sanitizeFields(field.fields, validRelationships);
if (field.type === 'tabs') {
field.tabs = field.tabs.map((tab) => {
const unsanitizedTab = { ...tab };
unsanitizedTab.fields = sanitizeFields(tab.fields, validRelationships);
return unsanitizedTab;
});
}
if ('blocks' in field && field.blocks) {
field.blocks = field.blocks.map((block) => {
const unsanitizedBlock = { ...block };

View File

@@ -168,20 +168,29 @@ export const radio = baseField.keys({
export const row = baseField.keys({
type: joi.string().valid('row').required(),
fields: joi.array().items(joi.link('#field')),
admin: baseAdminFields.keys({
description: joi.forbidden(),
readOnly: joi.forbidden(),
hidden: joi.forbidden(),
}),
admin: baseAdminFields.default(),
});
export const collapsible = baseField.keys({
label: joi.string().required(),
type: joi.string().valid('collapsible').required(),
fields: joi.array().items(joi.link('#field')),
admin: baseAdminFields.default(),
});
export const tabs = baseField.keys({
type: joi.string().valid('tabs').required(),
fields: joi.forbidden(),
tabs: joi.array().items(joi.object({
label: joi.string().required(),
fields: joi.array().items(joi.link('#field')).required(),
description: joi.alternatives().try(
joi.string(),
componentSchema,
),
})).required(),
admin: baseAdminFields.keys({
readOnly: joi.forbidden(),
hidden: joi.forbidden(),
description: joi.forbidden(),
}),
});
@@ -378,6 +387,7 @@ const fieldSchema = joi.alternatives()
array,
row,
collapsible,
tabs,
radio,
relationship,
checkbox,

View File

@@ -179,6 +179,18 @@ export type CollapsibleField = Omit<FieldBase, 'name'> & {
fields: Field[];
}
export type TabsAdmin = Omit<Admin, 'description'>;
export type TabsField = Omit<FieldBase, 'admin' | 'name'> & {
type: 'tabs';
tabs: {
label: string
fields: Field[];
description?: Description
}[]
admin?: TabsAdmin
}
export type UIField = {
name: string
label?: string
@@ -332,6 +344,7 @@ export type Field =
| PointField
| RowField
| CollapsibleField
| TabsField
| UIField;
export type FieldAffectingData =
@@ -369,6 +382,7 @@ export type NonPresentationalField = TextField
| CodeField
| PointField
| RowField
| TabsField
| CollapsibleField;
export type FieldWithPath = Field & {

View File

@@ -127,6 +127,22 @@ export const promise = async ({
break;
}
case 'tabs': {
field.tabs.forEach((tab) => {
traverseFields({
data,
doc,
fields: tab.fields,
operation,
promises,
req,
siblingData: siblingData || {},
siblingDoc: { ...siblingDoc },
});
});
break;
}
default: {
break;
}

View File

@@ -266,6 +266,26 @@ export const promise = async ({
break;
}
case 'tabs': {
field.tabs.forEach((tab) => {
traverseFields({
currentDepth,
depth,
doc,
fieldPromises,
fields: tab.fields,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
req,
siblingDoc,
showHiddenFields,
});
});
break;
}
default: {
break;
}

View File

@@ -282,6 +282,29 @@ export const promise = async ({
break;
}
case 'tabs': {
field.tabs.forEach((tab) => {
traverseFields({
data,
doc,
docWithLocales,
errors,
fields: tab.fields,
id,
mergeLocaleActions,
operation,
path,
promises,
req,
siblingData,
siblingDoc,
siblingDocWithLocales,
skipValidation: skipValidationFromHere,
});
});
break;
}
default: {
break;
}

View File

@@ -267,6 +267,25 @@ export const promise = async ({
break;
}
case 'tabs': {
field.tabs.forEach((tab) => {
traverseFields({
data,
doc,
fields: tab.fields,
id,
operation,
overrideAccess,
promises,
req,
siblingData,
siblingDoc,
});
});
break;
}
default: {
break;
}

View File

@@ -121,6 +121,19 @@ export const recurseNestedFields = ({
showHiddenFields,
});
}
} else if (field.type === 'tabs') {
field.tabs.forEach((tab) => {
recurseNestedFields({
promises,
data,
fields: tab.fields,
req,
overrideAccess,
depth,
currentDepth,
showHiddenFields,
});
});
} else if (Array.isArray(data[field.name])) {
if (field.type === 'blocks') {
data[field.name].forEach((row, i) => {

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, CollapsibleField } 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, TabsField } from '../../fields/config/types';
import { toWords } from '../../utilities/formatLabels';
import { Payload } from '../../index';
import { SanitizedCollectionConfig } from '../../collections/config/types';
@@ -151,6 +151,27 @@ function buildMutationInputType(payload: Payload, name: string, fields: Field[],
return acc;
}, []),
tabs: (field: TabsField) => field.tabs.reduce((acc, tab) => {
const test = [
...acc,
...tab.fields.reduce((subAcc, rowField: TabsField) => {
const getFieldSchema = fieldToSchemaMap[rowField.type];
if (getFieldSchema) {
const fieldSchema = getFieldSchema(rowField);
return [
...subAcc,
fieldSchema,
];
}
return subAcc;
}, []),
];
return test;
}, []),
};
const fieldTypes = fields.reduce((schema, field: Field) => {
@@ -160,21 +181,38 @@ function buildMutationInputType(payload: Payload, name: string, fields: Field[],
if (getFieldSchema) {
const fieldSchema = getFieldSchema(field);
if (fieldHasSubFields(field) && Array.isArray(fieldSchema)) {
return fieldSchema.reduce((acc, subField, i) => {
const currentSubField = field.fields[i];
if (fieldAffectsData(currentSubField)) {
if (Array.isArray(fieldSchema)) {
let subFields: Field[] = [];
if (fieldHasSubFields(field)) {
subFields = field.fields;
}
if (field.type === 'tabs') {
subFields = field.tabs.reduce((flattenedFields, tab) => {
return [
...flattenedFields,
...tab.fields,
];
}, []);
}
if (subFields.length > 0) {
return fieldSchema.reduce((acc, subField, i) => {
const currentSubField = subFields[i];
if (fieldAffectsData(currentSubField)) {
return {
...acc,
[currentSubField.name]: subField,
};
}
return {
...acc,
[currentSubField.name]: subField,
...fieldSchema,
};
}
return {
...acc,
...fieldSchema,
};
}, schema);
}, schema);
}
}
if (fieldAffectsData(field)) {

View File

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

View File

@@ -64,6 +64,15 @@ const buildFields = (label, fieldsToBuild) => fieldsToBuild.reduce((builtFields,
...subFields,
};
}
if (field.type === 'tabs') {
return field.tabs.reduce((fieldsWithTabFields, tab) => {
return {
...fieldsWithTabFields,
...buildFields(label, tab.fields),
};
}, { ...builtFields });
}
}
return builtFields;
}, {});

View File

@@ -35,7 +35,7 @@ const buildWhereInputType = (name: string, fields: Field[], parentName: string):
if (getFieldSchema) {
const fieldSchema = getFieldSchema(field);
if (fieldHasSubFields(field)) {
if (fieldHasSubFields(field) || field.type === 'tabs') {
return {
...schema,
...(fieldSchema.reduce((subFields, subField) => ({

View File

@@ -16,6 +16,7 @@ import {
NumberField, optionIsObject, PointField,
RadioField, RelationshipField,
RichTextField, RowField, SelectField,
TabsField,
TextareaField,
TextField, UploadField,
} from '../../fields/config/types';
@@ -274,6 +275,38 @@ const fieldToSchemaMap: (parentName: string) => any = (parentName: string) => ({
return rowSchema;
}, []),
tabs: (field: TabsField) => field.tabs.reduce((tabSchema, tab) => {
return [
...tabSchema,
...tab.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,
},
];
}
}
return rowSchema;
}, []),
];
}, []),
});
export default fieldToSchemaMap;

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, CollapsibleField } 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, TabsField } from '../fields/config/types';
import sortableFieldTypes from '../fields/sortableFieldTypes';
export type BuildSchemaOptions = {
@@ -329,6 +329,23 @@ const fieldToSchemaMap = {
}
});
return newFields;
},
tabs: (field: TabsField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
const newFields = { ...fields };
field.tabs.forEach((tab) => {
tab.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];
}
});
});
return newFields;
},
array: (field: ArrayField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions) => {