Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions src/components/Pega_Extensions_JsonEditor/Docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Meta, Story } from '@storybook/addon-docs/blocks';
import * as Stories from './demo.stories';

<Meta of={Stories} />

# JSON editor

`Pega_Extensions_JsonEditor` provides a multiline JSON field with syntax highlighting, validation, formatting, minification, copying, and clearing actions.

## Example

<Story of={Stories.BaseJsonEditor} />

## Usage

Use this component for editable JSON object or array values. The component updates the bound field through `getPConnect().getActionsApi()` and supports display-only and labels-left display modes.
175 changes: 175 additions & 0 deletions src/components/Pega_Extensions_JsonEditor/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
{
"name": "Pega_Extensions_JsonEditor",
"label": "JSON editor",
"description": "A multiline editor for JSON values with validation and formatting.",
"organization": "Pega",
"version": "5.0.4",
"library": "Extensions",
"allowedApplications": [],
"componentKey": "Pega_Extensions_JsonEditor",
"type": "Field",
"subtype": "Text-Paragraph",
"subtypeLabel": "Paragraph",
"icon": "images/pz-text-area-active.svg",
"properties": [
{
"name": "labelOption",
"label": "Label value",
"format": "SELECT",
"defaultValue": "custom",
"source": [
{
"key": "default",
"value": "Default"
},
{
"key": "custom",
"value": "Custom"
}
]
},
{
"name": "label",
"label": "Custom label value",
"format": "VALUEINPUT",
"required": true,
"ruleType": "Paragraph"
},
{
"name": "readOnly",
"label": "Edit mode",
"format": "READONLY"
},
{
"label": "Input settings",
"format": "GROUP",
"visibility": "(!readOnly = true)",
"properties": [
{
"name": "placeholder",
"label": "Placeholder",
"format": "TEXT",
"defaultValue": "{\n \"title\": \"Constellation\",\n \"count\": 3,\n \"enabled\": true,\n \"notes\": null,\n \"tags\": [\"json\", \"editor\"],\n \"settings\": {\n \"theme\": \"light\"\n }\n}"
},
{
"name": "helperText",
"label": "Helper text",
"format": "TEXT",
"defaultValue": "Supports objects, arrays, strings, numbers, booleans, and null."
}
]
},
{
"label": "Conditions",
"format": "GROUP",
"properties": [
{
"name": "required",
"label": "Required",
"format": "REQUIRED",
"visibility": "(!readOnly = true)"
},
{
"name": "disabled",
"label": "Disabled",
"format": "DISABLED",
"visibility": "(!readOnly = true)"
},
{
"name": "visibility",
"label": "Visibility",
"format": "VISIBILITY"
}
]
},
{
"name": "indent",
"label": "Indent spaces",
"format": "TEXT"
},
{
"label": "Editor actions",
"format": "GROUP",
"collapsible": true,
"properties": [
{
"name": "showValidate",
"label": "Show Validate action",
"format": "BOOLEAN"
},
{
"name": "showFormat",
"label": "Show Format action",
"format": "BOOLEAN"
},
{
"name": "showMinify",
"label": "Show Minify action",
"format": "BOOLEAN"
},
{
"name": "showCopy",
"label": "Show Copy action",
"format": "BOOLEAN"
},
{
"name": "showClear",
"label": "Show Clear action",
"format": "BOOLEAN"
},
{
"name": "showCharacterCount",
"label": "Show character count",
"format": "BOOLEAN"
},
{
"name": "showLineCount",
"label": "Show line count",
"format": "BOOLEAN"
}
]
},
{
"label": "Advanced",
"format": "GROUP",
"collapsible": true,
"properties": [
{
"name": "testId",
"label": "Test ID",
"format": "TEXT",
"ignorePattern": "[^-_\\p{N}\\p{L}]",
"includeAnnotations": false
}
]
},
{
"format": "LABEL",
"label": "About",
"name": "PegaAboutLabel",
"variant": "h3"
},
{
"format": "LABEL",
"label": "Pega_Extensions 5.0.4",
"name": "PegaAboutData",
"variant": "primary"
}
],
"defaultConfig": {
"label": "@L $this.label",
"indent": "2",
"showValidate": true,
"showFormat": true,
"showMinify": true,
"showCopy": true,
"showClear": true,
"showCharacterCount": true,
"showLineCount": true,
"detailFVLItem": true,
"isFormWidth": false,
"isContainerWidth": false
},
"infinityVersion": "26.1.1",
"packageCosmosVersion": "9.25.0"
}
59 changes: 59 additions & 0 deletions src/components/Pega_Extensions_JsonEditor/demo.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState } from 'react';
import type { Meta, StoryFn } from '@storybook/react-webpack5';

import { configProps, stateProps } from './mock';
import { PegaExtensionsJsonEditor } from './index';

const meta: Meta<typeof PegaExtensionsJsonEditor> = {
title: 'Fields/JSON editor',
component: PegaExtensionsJsonEditor,
excludeStories: /.*Data$/,
argTypes: {
getPConnect: {
table: {
disable: true,
},
},
displayMode: {
options: ['', 'DISPLAY_ONLY', 'LABELS_LEFT'],
control: {
type: 'select',
},
},
},
};

export default meta;

export const BaseJsonEditor: StoryFn<typeof PegaExtensionsJsonEditor> = (args) => {
const [value, setValue] = useState(configProps.value);

const props = {
value,
getPConnect: () =>
({
getStateProps: () => stateProps,
getActionsApi: () => ({
updateFieldValue: (_propName: string, nextValue: string) => setValue(nextValue),
}),
getLocalizedValue: (text: string) => text,
}) as unknown as typeof PConnect,
};

return <PegaExtensionsJsonEditor {...props} {...args} />;
};

BaseJsonEditor.args = {
label: configProps.label,
value: configProps.value,
helperText: configProps.helperText,
placeholder: configProps.placeholder,
testId: configProps.testId,
readOnly: configProps.readOnly,
disabled: configProps.disabled,
required: configProps.required,
status: configProps.status,
hideLabel: configProps.hideLabel,
displayMode: configProps.displayMode,
validatemessage: configProps.validatemessage,
};
118 changes: 118 additions & 0 deletions src/components/Pega_Extensions_JsonEditor/demo.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { composeStories } from '@storybook/react-webpack5';
import '@testing-library/jest-dom';

import * as DemoStories from './demo.stories';
import { configProps } from './mock';

const { BaseJsonEditor } = composeStories(DemoStories);

test('renders JsonEditor', async () => {
render(<BaseJsonEditor />);

expect(await screen.findByRole('textbox', { name: 'JSON value' })).toHaveValue(configProps.value);
expect(screen.getByRole('button', { name: 'Format JSON' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Validate JSON' })).toContainElement(
screen.getByTestId('JsonEditor-12345678:action:validate').querySelector('[data-icon-name="check"]'),
);
expect(screen.getByTestId('JsonEditor-12345678:action:format')).toContainElement(
screen.getByTestId('JsonEditor-12345678:action:format').querySelector('[data-icon-name="align-left"]'),
);
expect(screen.getByTestId('JsonEditor-12345678:action:minify')).toContainElement(
screen.getByTestId('JsonEditor-12345678:action:minify').querySelector('[data-icon-name="code"]'),
);
expect(screen.getByTestId('JsonEditor-12345678:action:copy')).toContainElement(
screen.getByTestId('JsonEditor-12345678:action:copy').querySelector('[data-icon-name="copy"]'),
);
expect(screen.getByTestId('JsonEditor-12345678:action:clear')).toContainElement(
screen.getByTestId('JsonEditor-12345678:action:clear').querySelector('[data-icon-name="trash"]'),
);
});

test('shows a placeholder covering all JSON value types', async () => {
render(<BaseJsonEditor value='' />);

expect(await screen.findByRole('textbox', { name: 'JSON value' })).toHaveAttribute(
'placeholder',
configProps.placeholder,
);
});

test('formats valid JSON', async () => {
render(<BaseJsonEditor />);

const editor = await screen.findByRole('textbox', { name: 'JSON value' });
fireEvent.change(editor, { target: { value: '{"name":"Pega"}' } });
fireEvent.click(screen.getByRole('button', { name: 'Format JSON' }));

expect(editor).toHaveValue('{\n "name": "Pega"\n}');
});

test('honors authoring visibility settings for actions and counters', async () => {
render(<BaseJsonEditor showValidate={false} showCopy={false} showCharacterCount={false} showLineCount={false} />);

expect(screen.queryByRole('button', { name: 'Validate JSON' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Copy JSON' })).not.toBeInTheDocument();
expect(screen.queryByText(/characters|lines/)).not.toBeInTheDocument();
});

test('renders syntax colors and line numbers', async () => {
render(<BaseJsonEditor />);

const editor = await screen.findByRole('textbox', { name: 'JSON value' });
fireEvent.change(editor, {
target: { value: '{\n "name": "Pega",\n "enabled": true\n}' },
});

expect(screen.getByTestId('JsonEditor-12345678:line-numbers').querySelectorAll('span')).toHaveLength(4);
expect(screen.getByTestId('JsonEditor-12345678:highlight').querySelectorAll('.json-key')).toHaveLength(2);
expect(screen.getByTestId('JsonEditor-12345678:highlight').querySelector('.json-literal')).toHaveTextContent('true');
});

test('reports invalid JSON and disables JSON transforms', async () => {
render(<BaseJsonEditor />);

const editor = await screen.findByRole('textbox', { name: 'JSON value' });
fireEvent.change(editor, { target: { value: '{invalid' } });

expect(screen.getByTestId('JsonEditor-12345678:status')).toHaveTextContent('Invalid JSON');
expect(screen.getByRole('button', { name: 'Format JSON' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Minify JSON' })).toBeDisabled();
});

test('reports the invalid JSON line and column', async () => {
render(<BaseJsonEditor />);

const editor = await screen.findByRole('textbox', { name: 'JSON value' });
fireEvent.change(editor, { target: { value: '{\n "name": "Pega",\n}' } });

expect(screen.getByTestId('JsonEditor-12345678:error')).toHaveTextContent('Line 3');
expect(screen.getByTestId('JsonEditor-12345678:error')).toHaveTextContent('column');
});

test('minifies and clears JSON', async () => {
render(<BaseJsonEditor />);

const editor = await screen.findByRole('textbox', { name: 'JSON value' });
fireEvent.change(editor, { target: { value: '{\n "name": "Pega"\n}' } });
fireEvent.click(screen.getByRole('button', { name: 'Minify JSON' }));
expect(editor).toHaveValue('{"name":"Pega"}');

fireEvent.click(screen.getByRole('button', { name: 'Clear JSON' }));
expect(editor).toHaveValue('');
expect(screen.queryByTestId('JsonEditor-12345678:status')).not.toBeInTheDocument();
});

test('copies JSON and reports copy success', async () => {
const writeText = jest.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText },
});

render(<BaseJsonEditor />);
fireEvent.click(screen.getByRole('button', { name: 'Copy JSON' }));

expect(await screen.findByRole('button', { name: 'Copied' })).toBeVisible();
expect(writeText).toHaveBeenCalledWith(configProps.value);
});
Loading
Loading