Skip to content

Commit

Permalink
Feat: InspectorFields component (#1547)
Browse files Browse the repository at this point in the history
* Feat: InspectorFields component

* Feat: getControlFields()

* Refactor: Fix getControlFields find call.

* Lint: Eslint fixes

* Test: Add Unit tests for InspectorFields.

Co-authored-by: Teresa Gobble <[email protected]>

* Refactor: (tests) Cleaning up InspectorFields.test

* Update InspectorFields.tsx

* CI/CD: Fix audit vulnerability.

---------

Co-authored-by: Teresa Gobble <[email protected]>
  • Loading branch information
theodesp and TeresaGobble authored Aug 30, 2023
1 parent bfdf19b commit 0fb5e9e
Show file tree
Hide file tree
Showing 9 changed files with 1,702 additions and 970 deletions.
2,401 changes: 1,441 additions & 960 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;
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 {};
1 change: 1 addition & 0 deletions packages/block-editor-utils/tests/components/Edit.test.tsx
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>,
]
`);
});
});

1 comment on commit 0fb5e9e

@headless-platform-by-wp-engine

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the recent updates to your Atlas environment:

App Environment URL Build
faustjs canary https://hg…wered.com ✅ (logs)

Learn more about building on Atlas in our documentation.

Please sign in to comment.