Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: InspectorFields component #1547

Merged
merged 8 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,390 changes: 1,434 additions & 956 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions packages/block-editor-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
"@faustwp/blocks": ">=2.0.0"
},
"dependencies": {
"@wordpress/blocks": "^12.15.0",
"@wordpress/components": "^25.4.0",
"@wordpress/block-editor": "^12.6.0",
"@wordpress/element": "5.16.0",
"@wordpress/hooks": "^3.38.0",
"@wordpress/i18n": "^4.38.0"
"@wordpress/blocks": "^12.17.0",
"@wordpress/components": "^25.6.0",
"@wordpress/block-editor": "^12.8.0",
"@wordpress/element": "5.17.0",
"@wordpress/hooks": "^3.40.0",
"@wordpress/i18n": "^4.40.0"
},
"devDependencies": {
"jest-environment-jsdom": "29.6.2",
Expand All @@ -25,8 +25,8 @@
"@types/jest": "^29.5.3",
"rimraf": "^4.4.0",
"@wordpress/jest-preset-default": "^11.9.0",
"@types/wordpress__blocks": "12.5.0",
"@types/wordpress__block-editor": "11.5.1",
"@types/wordpress__blocks": "12.5.2",
"@types/wordpress__block-editor": "11.5.2",
"ts-jest": "29.1.1",
"jest": "29.6.2",
"react": ">=18.0.0",
Expand Down
12 changes: 10 additions & 2 deletions packages/block-editor-utils/src/components/Edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@ import * as React from 'react';
import { useBlockProps } from '@wordpress/block-editor';
import { EditFnContext } from '../registerFaustBlock.js';
import Preview from './Preview.js';
import EditFormFields from './EditFormFields.js';
import getControlFields from '../helpers/getControlFields.js';

export default function Edit<T extends Record<string, any>>(
ctx: EditFnContext<T>,
) {
const blockProps = useBlockProps();
const { block, props } = ctx;
const { block, props, blockJson } = ctx;
const { editorFields = [] } = block.config;
const fieldsConfig = getControlFields(
blockJson,
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
editorFields,
);
return (
<div {...blockProps}>
{props.isSelected ? (
<div>Edit mode</div>
<EditFormFields props={props} fields={fieldsConfig} />
) : (
<Preview block={block} props={props} />
)}
Expand Down
26 changes: 26 additions & 0 deletions packages/block-editor-utils/src/components/EditFormFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';
import { BlockEditProps } from '@wordpress/blocks';
import InspectorFields from './InspectorFields.js';
import { Field } from '../types/index.js';

interface EditFormFieldsProps<T extends Record<string, any>> {
props: BlockEditProps<T>;
fields: Field[];
}

function EditFormFields<T extends Record<string, any>>({
props,
fields,
}: EditFormFieldsProps<T>) {
const inspectorFields = fields.filter(
(field: Field) => field.location === 'inspector',
);
return (
<>
<InspectorFields fields={inspectorFields} props={props} />
<div>Edit mode</div>
</>
);
}

export default EditFormFields;
43 changes: 43 additions & 0 deletions packages/block-editor-utils/src/components/InspectorFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody } from '@wordpress/components';
import { BlockEditProps } from '@wordpress/blocks';
import { applyFilters } from '@wordpress/hooks';
import { Control, Field } from '../types/index.js';

interface InspectorFieldsProps<T extends Record<string, any>> {
fields: Field[];
props: BlockEditProps<T>;
}

function InspectorFields<T extends Record<string, any>>({
fields,
props,
}: InspectorFieldsProps<T>) {
const loadedControls = applyFilters('faustBlockEditorUtils.controls', {}) as {
[key: string]: Control;
};
return (
<InspectorControls key="FaustBlockInspectorControls">
<>
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-call */}
{fields.map((field: Field) => {
const ControlField = loadedControls[field.control];
if (!ControlField) {
return null;
}
return (
<PanelBody
className="faust-inspector-form-field"
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
key={`inspector-controls-panel-${field.name}`}>
<ControlField config={field} props={props} />;
</PanelBody>
);
})}
</>
</InspectorControls>
);
}

export default InspectorFields;
56 changes: 56 additions & 0 deletions packages/block-editor-utils/src/helpers/getControlFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { BlockConfiguration } from '@wordpress/blocks';
import { Field, FieldControl, FieldType } from '../types/index.js';

const blockAttributeTypeToControlMap: Record<FieldType, FieldControl> = {
string: 'text',
boolean: 'radio',
integer: 'number',
number: 'number',
object: 'textarea',
array: 'textarea',
};

/**
* Returns a list of Field objects that describe how the Component Editor Fields configuration.
* Uses both the Block.json and the blocks editorFields config to create the final list.
* The logic is explained in detail in the RFC document for React Components To Blocks.
*
* @param blockJson Block.json object
* @param editorFields Block config editorFields metadata
* @returns
*/
function getControlFields(
blockJson: BlockConfiguration,
editorFields: Partial<Field>[],
): Field[] {
const fields: Field[] = [];
Object.entries(blockJson.attributes).forEach(([key, value]) => {
const fieldConfig = Object.entries(editorFields).find(([name]) => {
return key === name;
})?.[1];
const fieldType: FieldType = (value as any).type;
const control = blockAttributeTypeToControlMap[fieldType] ?? 'text';
// Set default field by merging both blockAttributes meta and editorFields hints.
if (fieldConfig) {
fields.push({
name: key,
label: fieldConfig.label ?? key,
type: fieldType,
location: fieldConfig.location ?? 'editor',
control: fieldConfig?.control ?? control,
});
} else {
// Set default field by using only blockAttributes meta
fields.push({
name: key,
label: key,
type: fieldType,
location: 'editor',
control,
});
}
});
return fields;
}

export default getControlFields;
6 changes: 3 additions & 3 deletions packages/block-editor-utils/src/registerFaustBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import DefaultSaveFn from './components/Save.js';
import DefaultEditFn from './components/Edit.js';
import { BlockFC, ConfigType } from './types/index.js';

export interface RegisterFaustBlockMetadata<P, T extends Record<string, any>> {
export interface RegisterFaustBlockMetadata<T extends Record<string, any>> {
// The block.json metadata object
blockJson: BlockConfiguration;
// A custom edit function
Expand Down Expand Up @@ -42,13 +42,13 @@ export interface SaveFnContext<T extends Record<string, unknown>> {
* @param block The React component to register as Gutenberg Block.
* @param ctx The metadata object that contains the block.json.
*/
export default function registerFaustBlock<P, T extends Record<string, any>>(
export default function registerFaustBlock<T extends Record<string, any>>(
block: BlockFC,
{
blockJson,
editFn = DefaultEditFn,
saveFn = DefaultSaveFn,
}: RegisterFaustBlockMetadata<P, T>,
}: RegisterFaustBlockMetadata<T>,
): ReturnType<typeof registerBlockType> {
// Pass the block config as a separate argument
const { config } = block;
Expand Down
20 changes: 20 additions & 0 deletions packages/block-editor-utils/src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ export type BlockFC<T = {}> = React.FC<T> & {
};
export interface ConfigType {
name?: string;
editorFields?: Partial<Field>[];
}

export type Field = {
name: string;
type: FieldType;
control: FieldControl;
location: FieldLocation;
label?: string;
default?: unknown;
}

export type FieldType = "string" | "number" | "boolean" | "integer" | "object" | "array"
export type FieldControl = "textarea" | "color" | "text" | "radio" | "select" | "range" | "number" | "checkbox"
export type FieldLocation = "editor" | "inspector"

export interface ControlProps<T extends Record<string, any>> {
config: Field;
props: BlockEditProps<T>;
}
export type Control = React.FC<ControlProps>

export {};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jest.mock('@wordpress/block-editor', () => {
const originalModule = jest.requireActual('@wordpress/block-editor');
return {
...originalModule,
InspectorControls: jest.fn((props) => <div>{props.children}</div>),
useBlockProps: jest.fn(),
};
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import InspectorFields from '../../src/components/InspectorFields.js';
import { actions, filters, addFilter } from '@wordpress/hooks';
import { Control, Field } from '../../src/types/index.js';

afterEach(() => {
jest.clearAllMocks();
});

jest.mock('@wordpress/block-editor', () => {
const originalModule = jest.requireActual('@wordpress/block-editor');
return {
...originalModule,
InspectorControls: jest.fn((props) => (
<div data-testid="inspector-controls-test">{props.children}</div>
)),
};
});
jest.mock('@wordpress/components', () => {
const originalModule = jest.requireActual('@wordpress/components');
return {
...originalModule,
PanelBody: jest.fn((props) => (
<div data-testid="panel-body-test">{props.children}</div>
)),
};
});

beforeEach(() => {
[actions, filters].forEach((hooks) => {
for (const k in hooks) {
if ('__current' === k) {
continue;
}

delete hooks[k];
}
delete hooks.all;
});
});

function filterA(controls: { [key: string]: Control }) {
// eslint-disable-next-line no-param-reassign
controls.color = () => <div>Another Color</div>;
return controls;
}

const blockProps = {
clientId: '1',
setAttributes: () => null,
context: {},
attributes: {
message: 'Hello',
},
isSelected: false,
className: 'SimpleBlock',
};

describe('<InspectorFields />', () => {
it('renders an empty InspectorFields if no fields are provided', () => {
const fields: Field[] = [];
addFilter('faustBlockEditorUtils.controls', 'my_callback', filterA);
render(<InspectorFields fields={fields} props={blockProps} />);
expect(screen.getByTestId('inspector-controls-test'))
.toMatchInlineSnapshot(`
<div
data-testid="inspector-controls-test"
/>
`);
});
it('renders InspectorFields if matching fields are provided', () => {
const fields: Field[] = [
{
type: 'string',
control: 'color',
name: 'myColor',
location: 'inspector',
},
{
type: 'string',
control: 'text',
name: 'myText',
location: 'inspector',
},
];
addFilter('faustBlockEditorUtils.controls', 'my_callback', filterA);
render(<InspectorFields fields={fields} props={blockProps} />);
expect(screen.getAllByText('Another Color')).toMatchInlineSnapshot(`
[
<div>
Another Color
</div>,
]
`);
});
});
Loading