feat: autolabel fields when label is omitted (#42)
* feat: autolabel fields when omitted * feat: handle autolabel in graphql mutation build * feat: autolabel blocks * test: add required slug field to blocks * feat: handle graphql names when label is false * feat: adds relationship field to test searchable input * feat: handle block cell type labeling pluralization * docs: remove all explicit labeling, no longer needed * fix: falsey column labels, allows false array labels * fix: client tests * fix: auto-labels globals * docs: globals auto-labeling and hooks clarification * fix; proper object type naming Co-authored-by: James <james@trbl.design>
This commit is contained in:
@@ -69,7 +69,7 @@ const ColumnSelector: React.FC<Props> = (props) => {
|
||||
pillStyle={isEnabled ? 'dark' : undefined}
|
||||
className={`${baseClass}__active-column`}
|
||||
>
|
||||
{field.label}
|
||||
{field.label || field.name}
|
||||
</Pill>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type Props = {
|
||||
label?: string | JSX.Element
|
||||
label?: string | false | JSX.Element
|
||||
required?: boolean
|
||||
htmlFor?: string
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ const RenderArray = React.memo((props: RenderArrayProps) => {
|
||||
key={row.key}
|
||||
id={row.key}
|
||||
blockType="array"
|
||||
label={label}
|
||||
label={labels.singular}
|
||||
isOpen={row.open}
|
||||
rowCount={rows.length}
|
||||
rowIndex={i}
|
||||
|
||||
@@ -7,6 +7,7 @@ export type Props = Omit<ArrayField, 'type'> & {
|
||||
path?: string
|
||||
fieldTypes: FieldTypes
|
||||
permissions: FieldPermissions
|
||||
label: string | false
|
||||
}
|
||||
|
||||
export type RenderArrayProps = {
|
||||
@@ -16,7 +17,7 @@ export type RenderArrayProps = {
|
||||
fields: Field[]
|
||||
permissions: FieldPermissions
|
||||
onDragEnd: (result: any) => void
|
||||
label: string
|
||||
label: string | false
|
||||
value: number
|
||||
readOnly: boolean
|
||||
minRows: number
|
||||
|
||||
@@ -15,7 +15,7 @@ export type RenderBlockProps = {
|
||||
fieldTypes: FieldTypes
|
||||
permissions: FieldPermissions
|
||||
onDragEnd: (result: any) => void
|
||||
label: string
|
||||
label: string | false
|
||||
value: number
|
||||
readOnly: boolean
|
||||
minRows: number
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react/jsx-max-props-per-line */
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import BlocksCell from './field-types/Blocks';
|
||||
@@ -13,6 +12,7 @@ describe('Cell Types', () => {
|
||||
name: 'blocks',
|
||||
labels: {
|
||||
singular: 'Block',
|
||||
plural: 'Blocks Content',
|
||||
},
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
@@ -30,14 +30,20 @@ describe('Cell Types', () => {
|
||||
{ blockType: 'number' },
|
||||
{ blockType: 'number' },
|
||||
];
|
||||
const { container } = render(<BlocksCell data={data} field={field} />);
|
||||
const { container } = render(<BlocksCell
|
||||
data={data}
|
||||
field={field}
|
||||
/>);
|
||||
const el = container.querySelector('span');
|
||||
expect(el).toHaveTextContent('2 Blocks Content - Number, Number');
|
||||
});
|
||||
|
||||
it('renders zero', () => {
|
||||
const data = [];
|
||||
const { container } = render(<BlocksCell data={data} field={field} />);
|
||||
const { container } = render(<BlocksCell
|
||||
data={data}
|
||||
field={field}
|
||||
/>);
|
||||
const el = container.querySelector('span');
|
||||
expect(el).toHaveTextContent('0 Blocks Content');
|
||||
});
|
||||
@@ -52,7 +58,10 @@ describe('Cell Types', () => {
|
||||
{ blockType: 'number' },
|
||||
];
|
||||
|
||||
const { container } = render(<BlocksCell data={data} field={field} />);
|
||||
const { container } = render(<BlocksCell
|
||||
data={data}
|
||||
field={field}
|
||||
/>);
|
||||
const el = container.querySelector('span');
|
||||
expect(el).toHaveTextContent('6 Blocks Content - Number, Number, Number, Number, Number and 1 more');
|
||||
});
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import React from 'react';
|
||||
import { ArrayField } from '../../../../../../../../fields/config/types';
|
||||
|
||||
const ArrayCell = ({ data, field }) => {
|
||||
type Props = {
|
||||
data: Record<string, unknown>
|
||||
field: ArrayField
|
||||
}
|
||||
|
||||
const ArrayCell: React.FC<Props> = ({ data, field }) => {
|
||||
const arrayFields = data ?? [];
|
||||
const label = `${arrayFields.length} ${field.label} rows`;
|
||||
const label = `${arrayFields.length} ${field?.labels?.plural || 'Rows'}`;
|
||||
|
||||
return (
|
||||
<span>{label}</span>
|
||||
|
||||
@@ -4,7 +4,7 @@ const BlocksCell = ({ data, field }) => {
|
||||
const selectedBlocks = data ? data.map(({ blockType }) => blockType) : [];
|
||||
const blockLabels = field.blocks.map((s) => ({ slug: s.slug, label: s.labels.singular }));
|
||||
|
||||
let label = `0 ${field.label}`;
|
||||
let label = `0 ${field.labels.plural}`;
|
||||
|
||||
const formatBlockList = (blocks) => blocks.map((b) => {
|
||||
const filtered = blockLabels.filter((f) => f.slug === b)?.[0];
|
||||
@@ -14,9 +14,9 @@ const BlocksCell = ({ data, field }) => {
|
||||
const itemsToShow = 5;
|
||||
if (selectedBlocks.length > itemsToShow) {
|
||||
const more = selectedBlocks.length - itemsToShow;
|
||||
label = `${selectedBlocks.length} ${field.label} - ${formatBlockList(selectedBlocks.slice(0, itemsToShow))} and ${more} more`;
|
||||
label = `${selectedBlocks.length} ${field.labels.plural} - ${formatBlockList(selectedBlocks.slice(0, itemsToShow))} and ${more} more`;
|
||||
} else if (selectedBlocks.length > 0) {
|
||||
label = `${selectedBlocks.length} ${field.label} - ${formatBlockList(selectedBlocks)}`;
|
||||
label = `${selectedBlocks.length} ${selectedBlocks.length === 1 ? field.labels.singular : field.labels.plural} - ${formatBlockList(selectedBlocks)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -36,7 +36,7 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
if (!CellComponent) {
|
||||
return (
|
||||
<WrapElement {...wrapElementProps}>
|
||||
{(cellData === '' || typeof cellData === 'undefined') && `<No ${field.label ?? 'data'}>`}
|
||||
{(cellData === '' || typeof cellData === 'undefined') && `<No ${typeof field.label === 'string' ? field.label : 'data'}>`}
|
||||
{typeof cellData === 'string' && cellData}
|
||||
{typeof cellData === 'number' && cellData}
|
||||
{typeof cellData === 'object' && JSON.stringify(cellData)}
|
||||
|
||||
@@ -51,7 +51,7 @@ const buildColumns = (collection: CollectionConfig, columns: string[], setSort:
|
||||
components: {
|
||||
Heading: (
|
||||
<SortColumn
|
||||
label={field.label}
|
||||
label={field.label || field.name}
|
||||
name={field.name}
|
||||
handleChange={setSort}
|
||||
disable={field.disableSort || undefined}
|
||||
|
||||
@@ -8,7 +8,7 @@ import baseVerificationFields from '../../fields/baseFields/baseVerificationFiel
|
||||
import baseAccountLockFields from '../../fields/baseFields/baseAccountLockFields';
|
||||
import baseUploadFields from '../../fields/baseFields/baseUploadFields';
|
||||
import baseImageUploadFields from '../../fields/baseFields/baseImageUploadFields';
|
||||
import formatLabels from '../../utilities/formatLabels';
|
||||
import { formatLabels } from '../../utilities/formatLabels';
|
||||
import { defaults, authDefaults } from './defaults';
|
||||
|
||||
const mergeBaseFields = (fields, baseFields) => {
|
||||
@@ -62,7 +62,7 @@ const sanitizeCollection = (collections: PayloadCollectionConfig[], collection:
|
||||
const sanitized: PayloadCollectionConfig = merge(defaults, collection);
|
||||
|
||||
sanitized.slug = toKebabCase(sanitized.slug);
|
||||
sanitized.labels = !sanitized.labels ? formatLabels(sanitized.slug) : sanitized.labels;
|
||||
sanitized.labels = sanitized.labels || formatLabels(sanitized.slug);
|
||||
|
||||
if (sanitized.upload) {
|
||||
if (sanitized.upload === true) sanitized.upload = {};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Config } from '../config/types';
|
||||
import APIError from './APIError';
|
||||
|
||||
class MissingGlobalLabel extends APIError {
|
||||
constructor(config: Config) {
|
||||
super(`${config.globals} object is missing label`);
|
||||
}
|
||||
}
|
||||
|
||||
export default MissingGlobalLabel;
|
||||
@@ -13,6 +13,5 @@ export { default as MissingCollectionLabel } from './MissingCollectionLabel';
|
||||
export { default as MissingFieldInputOptions } from './MissingFieldInputOptions';
|
||||
export { default as MissingFieldType } from './MissingFieldType';
|
||||
export { default as MissingFile } from './MissingFile';
|
||||
export { default as MissingGlobalLabel } from './MissingGlobalLabel';
|
||||
export { default as NotFound } from './NotFound';
|
||||
export { default as ValidationError } from './ValidationError';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import sanitizeFields from './sanitize';
|
||||
import { MissingFieldType, InvalidFieldRelationship } from '../../errors';
|
||||
import { Block } from './types';
|
||||
|
||||
describe('sanitizeFields', () => {
|
||||
it('should throw on missing type field', () => {
|
||||
@@ -11,6 +12,39 @@ describe('sanitizeFields', () => {
|
||||
sanitizeFields(fields, []);
|
||||
}).toThrow(MissingFieldType);
|
||||
});
|
||||
it('should populate label if missing', () => {
|
||||
const fields = [{
|
||||
name: 'someCollection',
|
||||
type: 'text',
|
||||
}];
|
||||
const sanitizedField = sanitizeFields(fields, [])[0];
|
||||
expect(sanitizedField.name).toStrictEqual('someCollection');
|
||||
expect(sanitizedField.label).toStrictEqual('Some Collection');
|
||||
expect(sanitizedField.type).toStrictEqual('text');
|
||||
});
|
||||
it('should allow auto-label override', () => {
|
||||
const fields = [{
|
||||
name: 'someCollection',
|
||||
type: 'text',
|
||||
label: 'Do not label',
|
||||
}];
|
||||
const sanitizedField = sanitizeFields(fields, [])[0];
|
||||
expect(sanitizedField.name).toStrictEqual('someCollection');
|
||||
expect(sanitizedField.label).toStrictEqual('Do not label');
|
||||
expect(sanitizedField.type).toStrictEqual('text');
|
||||
});
|
||||
it('should allow label opt-out', () => {
|
||||
const fields = [{
|
||||
name: 'someCollection',
|
||||
type: 'text',
|
||||
label: false,
|
||||
}];
|
||||
const sanitizedField = sanitizeFields(fields, [])[0];
|
||||
expect(sanitizedField.name).toStrictEqual('someCollection');
|
||||
expect(sanitizedField.label).toStrictEqual(false);
|
||||
expect(sanitizedField.type).toStrictEqual('text');
|
||||
});
|
||||
|
||||
|
||||
describe('relationships', () => {
|
||||
it('should not throw on valid relationship', () => {
|
||||
@@ -41,6 +75,15 @@ describe('sanitizeFields', () => {
|
||||
|
||||
it('should not throw on valid relationship inside blocks', () => {
|
||||
const validRelationships = ['some-collection'];
|
||||
const relationshipBlock: Block = {
|
||||
slug: 'relationshipBlock',
|
||||
fields: [{
|
||||
type: 'relationship',
|
||||
label: 'my-relationship',
|
||||
name: 'My Relationship',
|
||||
relationTo: 'some-collection',
|
||||
}],
|
||||
};
|
||||
const fields = [{
|
||||
name: 'layout',
|
||||
label: 'Layout Blocks',
|
||||
@@ -48,14 +91,7 @@ describe('sanitizeFields', () => {
|
||||
singular: 'Block',
|
||||
},
|
||||
type: 'blocks',
|
||||
blocks: [{
|
||||
fields: [{
|
||||
type: 'relationship',
|
||||
label: 'my-relationship',
|
||||
name: 'My Relationship',
|
||||
relationTo: 'some-collection',
|
||||
}],
|
||||
}],
|
||||
blocks: [relationshipBlock],
|
||||
}];
|
||||
expect(() => {
|
||||
sanitizeFields(fields, validRelationships);
|
||||
@@ -90,6 +126,15 @@ describe('sanitizeFields', () => {
|
||||
|
||||
it('should throw on invalid relationship inside blocks', () => {
|
||||
const validRelationships = ['some-collection'];
|
||||
const relationshipBlock: Block = {
|
||||
slug: 'relationshipBlock',
|
||||
fields: [{
|
||||
type: 'relationship',
|
||||
label: 'my-relationship',
|
||||
name: 'My Relationship',
|
||||
relationTo: 'not-valid',
|
||||
}],
|
||||
};
|
||||
const fields = [{
|
||||
name: 'layout',
|
||||
label: 'Layout Blocks',
|
||||
@@ -97,14 +142,7 @@ describe('sanitizeFields', () => {
|
||||
singular: 'Block',
|
||||
},
|
||||
type: 'blocks',
|
||||
blocks: [{
|
||||
fields: [{
|
||||
type: 'relationship',
|
||||
label: 'my-relationship',
|
||||
name: 'My Relationship',
|
||||
relationTo: 'not-valid',
|
||||
}],
|
||||
}],
|
||||
blocks: [relationshipBlock],
|
||||
}];
|
||||
expect(() => {
|
||||
sanitizeFields(fields, validRelationships);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { formatLabels, toWords } from '../../utilities/formatLabels';
|
||||
import { MissingFieldType, InvalidFieldRelationship } from '../../errors';
|
||||
import validations from '../validations';
|
||||
|
||||
const sanitizeFields = (fields, validRelationships) => {
|
||||
const sanitizeFields = (fields, validRelationships: string[]) => {
|
||||
if (!fields) return [];
|
||||
|
||||
return fields.map((unsanitizedField) => {
|
||||
@@ -9,15 +10,24 @@ const sanitizeFields = (fields, validRelationships) => {
|
||||
|
||||
if (!field.type) throw new MissingFieldType(field);
|
||||
|
||||
// Auto-label
|
||||
if (field.name && typeof field.label !== 'string' && field.label !== false) {
|
||||
field.label = toWords(field.name);
|
||||
}
|
||||
|
||||
if (field.type === 'relationship') {
|
||||
const relationships = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo];
|
||||
relationships.forEach((relationship) => {
|
||||
relationships.forEach((relationship: string) => {
|
||||
if (!validRelationships.includes(relationship)) {
|
||||
throw new InvalidFieldRelationship(field, relationship);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
field.labels = field.labels || formatLabels(field.name);
|
||||
}
|
||||
|
||||
if (typeof field.validate === 'undefined') {
|
||||
const defaultValidate = validations[field.type];
|
||||
if (defaultValidate) {
|
||||
@@ -36,6 +46,7 @@ const sanitizeFields = (fields, validRelationships) => {
|
||||
if (field.blocks) {
|
||||
field.blocks = field.blocks.map((block) => {
|
||||
const unsanitizedBlock = { ...block };
|
||||
unsanitizedBlock.labels = !unsanitizedBlock.labels ? formatLabels(unsanitizedBlock.slug) : unsanitizedBlock.labels;
|
||||
unsanitizedBlock.fields = sanitizeFields(block.fields, validRelationships);
|
||||
return unsanitizedBlock;
|
||||
});
|
||||
|
||||
@@ -21,7 +21,10 @@ export const baseAdminFields = joi.object().keys({
|
||||
});
|
||||
|
||||
export const baseField = joi.object().keys({
|
||||
label: joi.string(),
|
||||
label: joi.alternatives().try(
|
||||
joi.string(),
|
||||
joi.valid(false),
|
||||
),
|
||||
required: joi.boolean().default(false),
|
||||
saveToJWT: joi.boolean().default(false),
|
||||
unique: joi.boolean().default(false),
|
||||
|
||||
@@ -51,7 +51,7 @@ export type Option = OptionObject | string
|
||||
|
||||
export interface FieldBase {
|
||||
name?: string;
|
||||
label?: string;
|
||||
label?: string | false;
|
||||
required?: boolean;
|
||||
unique?: boolean;
|
||||
index?: boolean;
|
||||
@@ -209,7 +209,7 @@ export type RadioField = FieldBase & {
|
||||
|
||||
export type Block = {
|
||||
slug: string,
|
||||
labels: Labels
|
||||
labels?: Labels
|
||||
fields: Field[],
|
||||
imageURL?: string
|
||||
imageAltText?: string
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { MissingGlobalLabel } from '../../errors';
|
||||
import { toWords } from '../../utilities/formatLabels';
|
||||
import { PayloadCollectionConfig } from '../../collections/config/types';
|
||||
import sanitizeFields from '../../fields/config/sanitize';
|
||||
import { PayloadGlobalConfig, GlobalConfig } from './types';
|
||||
|
||||
const sanitizeGlobals = (collections, globals) => {
|
||||
// /////////////////////////////////
|
||||
// Ensure globals are valid
|
||||
// /////////////////////////////////
|
||||
|
||||
globals.forEach((globalConfig) => {
|
||||
if (!globalConfig.label) {
|
||||
throw new MissingGlobalLabel(globalConfig);
|
||||
}
|
||||
});
|
||||
|
||||
const sanitizeGlobals = (collections: PayloadCollectionConfig[], globals: PayloadGlobalConfig[]): GlobalConfig[] => {
|
||||
const sanitizedGlobals = globals.map((global) => {
|
||||
const sanitizedGlobal = { ...global };
|
||||
|
||||
sanitizedGlobal.label = sanitizedGlobal.label || toWords(sanitizedGlobal.slug);
|
||||
|
||||
// /////////////////////////////////
|
||||
// Ensure that collection has required object structure
|
||||
// /////////////////////////////////
|
||||
@@ -36,7 +30,7 @@ const sanitizeGlobals = (collections, globals) => {
|
||||
const validRelationships = collections.map((c) => c.slug);
|
||||
sanitizedGlobal.fields = sanitizeFields(global.fields, validRelationships);
|
||||
|
||||
return sanitizedGlobal;
|
||||
return sanitizedGlobal as GlobalConfig;
|
||||
});
|
||||
|
||||
return sanitizedGlobals;
|
||||
|
||||
@@ -4,6 +4,13 @@ import fieldSchema from '../../fields/config/schema';
|
||||
const schema = joi.object().keys({
|
||||
slug: joi.string().required(),
|
||||
label: joi.string(),
|
||||
hooks: joi.object({
|
||||
beforeValidate: joi.array().items(joi.func()),
|
||||
beforeChange: joi.array().items(joi.func()),
|
||||
afterChange: joi.array().items(joi.func()),
|
||||
beforeRead: joi.array().items(joi.func()),
|
||||
afterRead: joi.array().items(joi.func()),
|
||||
}),
|
||||
access: joi.object({
|
||||
read: joi.func(),
|
||||
update: joi.func(),
|
||||
|
||||
@@ -1,20 +1,55 @@
|
||||
import React from 'react';
|
||||
import { Model, Document } from 'mongoose';
|
||||
import { DeepRequired } from 'ts-essentials';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { Access } from '../../config/types';
|
||||
import { Field } from '../../fields/config/types';
|
||||
|
||||
export type BeforeValidateHook = (args?: {
|
||||
data?: any;
|
||||
req?: PayloadRequest;
|
||||
originalDoc?: any;
|
||||
}) => any;
|
||||
|
||||
export type BeforeChangeHook = (args?: {
|
||||
data: any;
|
||||
req: PayloadRequest;
|
||||
originalDoc?: any;
|
||||
}) => any;
|
||||
|
||||
export type AfterChangeHook = (args?: {
|
||||
doc: any;
|
||||
req: PayloadRequest;
|
||||
}) => any;
|
||||
|
||||
export type BeforeReadHook = (args?: {
|
||||
doc: any;
|
||||
req: PayloadRequest;
|
||||
query: { [key: string]: any };
|
||||
}) => any;
|
||||
|
||||
export type AfterReadHook = (args?: {
|
||||
doc: any;
|
||||
req: PayloadRequest;
|
||||
query?: { [key: string]: any };
|
||||
}) => any;
|
||||
|
||||
export type GlobalModel = Model<Document>
|
||||
|
||||
export type PayloadGlobalConfig = {
|
||||
slug: string
|
||||
label?: string
|
||||
preview?: (doc: Document, token: string) => string
|
||||
hooks?: {
|
||||
beforeValidate?: BeforeValidateHook[]
|
||||
beforeChange?: BeforeChangeHook[]
|
||||
afterChange?: AfterChangeHook[]
|
||||
beforeRead?: BeforeReadHook[]
|
||||
afterRead?: AfterReadHook[]
|
||||
}
|
||||
access?: {
|
||||
create?: Access;
|
||||
read?: Access;
|
||||
update?: Access;
|
||||
delete?: Access;
|
||||
admin?: Access;
|
||||
}
|
||||
fields: Field[];
|
||||
|
||||
@@ -15,6 +15,7 @@ import withNullableType from './withNullableType';
|
||||
import formatName from '../utilities/formatName';
|
||||
import combineParentName from '../utilities/combineParentName';
|
||||
import { ArrayField, Field, FieldWithSubFields, GroupField, RelationshipField, RowField, SelectField } from '../../fields/config/types';
|
||||
import { toWords } from '../../utilities/formatLabels';
|
||||
|
||||
function buildMutationInputType(name: string, fields: Field[], parentName: string, forceNullable = false): GraphQLInputObjectType {
|
||||
const fieldToSchemaMap = {
|
||||
@@ -68,7 +69,7 @@ function buildMutationInputType(name: string, fields: Field[], parentName: strin
|
||||
let type: PayloadGraphQLRelationshipType = GraphQLString;
|
||||
|
||||
if (Array.isArray(relationTo)) {
|
||||
const fullName = `${combineParentName(parentName, field.label)}RelationshipInput`;
|
||||
const fullName = `${combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label)}RelationshipInput`;
|
||||
type = new GraphQLInputObjectType({
|
||||
name: fullName,
|
||||
fields: {
|
||||
@@ -91,14 +92,14 @@ function buildMutationInputType(name: string, fields: Field[], parentName: strin
|
||||
return { type: field.hasMany ? new GraphQLList(type) : type };
|
||||
},
|
||||
array: (field: ArrayField) => {
|
||||
const fullName = combineParentName(parentName, field.label);
|
||||
const fullName = combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label);
|
||||
let type: GraphQLType | GraphQLList<GraphQLType> = buildMutationInputType(fullName, field.fields, fullName);
|
||||
type = new GraphQLList(withNullableType(field, type, forceNullable));
|
||||
return { type };
|
||||
},
|
||||
group: (field: GroupField) => {
|
||||
const requiresAtLeastOneField = field.fields.some((subField) => (subField.required && !subField.localized));
|
||||
const fullName = combineParentName(parentName, field.label);
|
||||
const fullName = combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label);
|
||||
let type: GraphQLType = buildMutationInputType(fullName, field.fields, fullName);
|
||||
if (requiresAtLeastOneField) type = new GraphQLNonNull(type);
|
||||
return { type };
|
||||
|
||||
@@ -13,11 +13,12 @@ import {
|
||||
GraphQLUnionType,
|
||||
} from 'graphql';
|
||||
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars';
|
||||
import { Field, RadioField, RelationshipField, SelectField, UploadField, optionIsObject } from '../../fields/config/types';
|
||||
import { Field, RadioField, RelationshipField, SelectField, UploadField, optionIsObject, ArrayField, GroupField, BlockField, RowField } from '../../fields/config/types';
|
||||
import formatName from '../utilities/formatName';
|
||||
import combineParentName from '../utilities/combineParentName';
|
||||
import withNullableType from './withNullableType';
|
||||
import { BaseFields } from '../../collections/graphql/types';
|
||||
import { toWords } from '../../utilities/formatLabels';
|
||||
|
||||
type LocaleInputType = {
|
||||
locale: {
|
||||
@@ -44,7 +45,8 @@ function buildObjectType(name: string, fields: Field[], parentName: string, base
|
||||
date: (field: Field) => ({ type: withNullableType(field, DateTimeResolver) }),
|
||||
upload: (field: UploadField) => {
|
||||
const { relationTo, label } = field;
|
||||
const uploadName = combineParentName(parentName, label);
|
||||
|
||||
const uploadName = combineParentName(parentName, label === false ? toWords(field.name, true) : label);
|
||||
|
||||
// If the relationshipType is undefined at this point,
|
||||
// it can be assumed that this blockType can have a relationship
|
||||
@@ -186,7 +188,7 @@ function buildObjectType(name: string, fields: Field[], parentName: string, base
|
||||
const { relationTo, label } = field;
|
||||
const isRelatedToManyCollections = Array.isArray(relationTo);
|
||||
const hasManyValues = field.hasMany;
|
||||
const relationshipName = combineParentName(parentName, label);
|
||||
const relationshipName = combineParentName(parentName, label === false ? toWords(field.name, true) : label);
|
||||
|
||||
let type;
|
||||
let relationToType = null;
|
||||
@@ -406,15 +408,15 @@ function buildObjectType(name: string, fields: Field[], parentName: string, base
|
||||
|
||||
return relationship;
|
||||
},
|
||||
array: (field) => {
|
||||
const fullName = combineParentName(parentName, field.label);
|
||||
array: (field: ArrayField) => {
|
||||
const fullName = combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label);
|
||||
let type = recursiveBuildObjectType(fullName, field.fields, fullName);
|
||||
type = new GraphQLList(withNullableType(field, type));
|
||||
|
||||
return { type };
|
||||
},
|
||||
group: (field) => {
|
||||
const fullName = combineParentName(parentName, field.label);
|
||||
group: (field: GroupField) => {
|
||||
const fullName = combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label);
|
||||
const type = recursiveBuildObjectType(fullName, field.fields, fullName);
|
||||
|
||||
return { type };
|
||||
@@ -425,8 +427,10 @@ function buildObjectType(name: string, fields: Field[], parentName: string, base
|
||||
return this.types.blockTypes[block.slug];
|
||||
});
|
||||
|
||||
const fullName = combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label);
|
||||
|
||||
const type = new GraphQLList(new GraphQLUnionType({
|
||||
name: combineParentName(parentName, field.label),
|
||||
name: fullName,
|
||||
types: blockTypes,
|
||||
resolveType: (data) => this.types.blockTypes[data.blockType].name,
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import formatLabels from './formatLabels';
|
||||
import { formatLabels, toWords } from './formatLabels';
|
||||
|
||||
describe('formatLabels', () => {
|
||||
it('should format singular slug', () => {
|
||||
@@ -28,4 +28,14 @@ describe('formatLabels', () => {
|
||||
plural: 'Camel Case Items',
|
||||
});
|
||||
});
|
||||
|
||||
describe('toWords', () => {
|
||||
it('should convert camel to capitalized words', () => {
|
||||
expect(toWords('camelCaseItems')).toBe('Camel Case Items');
|
||||
});
|
||||
|
||||
it('should allow no separator (used for building GraphQL label from name)', () => {
|
||||
expect(toWords('myGraphField', true)).toBe('MyGraphField');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import pluralize, { isPlural, singular } from 'pluralize';
|
||||
|
||||
const capitalizeFirstLetter = (string: string) => string.charAt(0).toUpperCase() + string.slice(1);
|
||||
const capitalizeFirstLetter = (string: string): string => string.charAt(0).toUpperCase() + string.slice(1);
|
||||
|
||||
const toWords = (inputString: string): string => {
|
||||
const toWords = (inputString: string, joinWords = false): string => {
|
||||
const notNullString = inputString || '';
|
||||
const trimmedString = notNullString.trim();
|
||||
const arrayOfStrings = trimmedString.split(/[\s-]/);
|
||||
@@ -15,10 +15,12 @@ const toWords = (inputString: string): string => {
|
||||
}
|
||||
});
|
||||
|
||||
return splitStringsArray.join(' ');
|
||||
return joinWords
|
||||
? splitStringsArray.join('').replace(/\s/gi, '')
|
||||
: splitStringsArray.join(' ');
|
||||
};
|
||||
|
||||
const formatLabels = ((slug: string): { singular: string, plural: string} => {
|
||||
const formatLabels = ((slug: string): { singular: string, plural: string } => {
|
||||
const words = toWords(slug);
|
||||
return (isPlural(slug))
|
||||
? {
|
||||
@@ -31,4 +33,7 @@ const formatLabels = ((slug: string): { singular: string, plural: string} => {
|
||||
};
|
||||
});
|
||||
|
||||
export default formatLabels;
|
||||
export {
|
||||
formatLabels,
|
||||
toWords,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user