Skip to content

Commit 755896e

Browse files
committed
feat(forms): bring forms library into monorepo and migrate to Carbon
1 parent 0a0a0df commit 755896e

188 files changed

Lines changed: 14856 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"author": "The Kaoto Team",
77
"license": "Apache License v2.0",
88
"workspaces": [
9-
"packages/*"
9+
"packages/*",
10+
"!packages/forms"
1011
],
1112
"resolutions": {
1213
"@patternfly/patternfly": "^6.6.1",

‎packages/forms/README.md‎

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<h1 align="center">
2+
<a href="https://kaoto.io/"><img style="width: 400px;" src="../ui/src/assets/logo-kaoto.png" alt="Kaoto"/></a>
3+
</h1>
4+
5+
<p align=center>
6+
<a href="https://github.com/KaotoIO/kaoto/blob/main/LICENSE"><img src="https://img.shields.io/github/license/KaotoIO/kaoto?color=blue&style=for-the-badge" alt="License"/></a>
7+
<a href="https://www.youtube.com/@KaotoIO"><img src="https://img.shields.io/badge/Youtube-Follow-brightgreen?color=red&style=for-the-badge" alt="Follow on Youtube"></a>
8+
<a href="https://camel.zulipchat.com/#narrow/stream/441302-kaoto"><img src="https://img.shields.io/badge/zulip-join_chat-brightgreen?color=yellow&style=for-the-badge" alt="Zulip"/></a>
9+
<a href="https://kaoto.io"><img src="https://img.shields.io/badge/Kaoto.io-Visit-white?color=indigo&style=for-the-badge" alt="Kaoto.io"/></a>
10+
</p><br/>
11+
12+
<h2 align="center">Kaoto - The Integration Designer for <a href="https://camel.apache.org">Apache Camel</a></h2>
13+
14+
<p align="center">
15+
<a style="font-weight: bold" href="https://kaoto.io/docs/installation">Documentation</a> |
16+
<a style="font-weight: bold" href="https://kaoto.io/workshop/">Workshops</a> |
17+
<a style="font-weight: bold" href="https://kaoto.io/contribute/">Contribute</a> |
18+
<a style="font-weight: bold" href="https://camel.zulipchat.com/#narrow/stream/441302-kaoto">Chat</a>
19+
</p>
20+
21+
# Kaoto forms
22+
23+
Kaoto forms (`@kaoto/forms`) is a library of reusable React components to build forms from a JSON schema. It is designed to be flexible and easy to use, and it is adapted to Apache Camel component schemas. The UI is built with the [Carbon Design System](https://carbondesignsystem.com/).
24+
25+
# Installation
26+
27+
```bash
28+
yarn add @kaoto/forms
29+
```
30+
31+
The library does not bundle its UI dependencies. Install the following peer dependencies in your application:
32+
33+
- `react` and `react-dom`
34+
- `@carbon/react`
35+
- `@carbon/styles`
36+
- `@carbon/icons-react`
37+
- `@carbon/ibm-products`
38+
39+
See [`package.json`](./package.json) for the supported version ranges.
40+
41+
# Getting started
42+
43+
```tsx
44+
import { KaotoForm } from '@kaoto/forms';
45+
import { JSONSchema4 } from 'json-schema';
46+
47+
const schema: JSONSchema4 = {
48+
type: 'object',
49+
properties: {
50+
name: {
51+
type: 'string',
52+
title: 'Name',
53+
description: 'The name of the integration',
54+
},
55+
description: {
56+
type: 'string',
57+
title: 'Description',
58+
description: 'A brief description of the integration',
59+
},
60+
// Add more properties as needed
61+
},
62+
};
63+
64+
const model = {
65+
name: 'My Integration',
66+
description: 'This is a sample integration',
67+
};
68+
69+
export const MyForm = () => {
70+
const handleOnChangeProp = (propName: string, value: unknown) => {
71+
console.log(`Property ${propName} changed to`, value);
72+
};
73+
74+
const handleOnChange = (value: unknown) => {
75+
console.log('Form changed:', value);
76+
};
77+
78+
return <KaotoForm schema={schema} model={model} onChangeProp={handleOnChangeProp} onChange={handleOnChange} />;
79+
};
80+
```
81+
82+
# `KaotoForm` props
83+
84+
| Prop | Type | Description |
85+
| --------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
86+
| `schema` | `JSONSchema4` | The JSON schema describing the form. **Required**: the component throws if it is not provided. |
87+
| `model` | `unknown` | The current value of the form. The form re-syncs whenever a new `model` reference is provided. |
88+
| `onChange` | `(value: unknown) => void` | Called with the whole updated model every time the user edits the form. It is not called when the consumer provides a new `model`. |
89+
| `onChangeProp` | `(propName: string, value: unknown) => void` | Called with the path and the new value of the individual property that changed. |
90+
| `omitFields` | `string[]` | Property names to hide from the form. Defaults to `[]`. |
91+
| `disabled` | `boolean` | Disables all the form fields. |
92+
| `withLayer` | `boolean` | Wraps the form in a Carbon `<Layer>` when no ancestor provides one, so the inputs don't blend with the page background. Defaults to `true`. |
93+
| `customFieldsFactory` | `(schema: JSONSchema4) => FunctionComponent<FieldProps> \| undefined` | Overrides the field selection. Return a custom field component for a given schema, or `undefined` to use the default one. |
94+
| `data-testid` | `string` | Test id set on the form element. |
95+
96+
## Validating the form
97+
98+
`KaotoForm` forwards a ref exposing a `validate()` method. It validates the current model against the schema, displays the errors in the form and returns them, or `null` when the model is valid.
99+
100+
```tsx
101+
import { KaotoForm, KaotoFormApi } from '@kaoto/forms';
102+
import { useRef } from 'react';
103+
104+
export const MyForm = () => {
105+
const formRef = useRef<KaotoFormApi>(null);
106+
107+
const onSave = () => {
108+
const errors = formRef.current?.validate(); // Record<string, string[]> | null
109+
if (errors === null) {
110+
// The model is valid
111+
}
112+
};
113+
114+
return <KaotoForm ref={formRef} schema={schema} model={model} />;
115+
};
116+
```
117+
118+
# Other exports
119+
120+
Besides `KaotoForm`, the package exports the building blocks used to compose it:
121+
122+
- **Fields**: the individual field components, such as `AutoField`.
123+
- **`Typeahead`** and **`KeyValue`** components.
124+
- **Hooks**: for example, `useFieldValue` and the `oneOf` helpers.
125+
- **Providers**: model, schema, field-filtering, canvas form tabs and form component factory providers, plus `SuggestionRegistryProvider` and the suggestions API to offer value suggestions in the fields.
126+
- **Utilities**: helpers to work with schemas and models, and the validation helpers `getValidator` and `errorsMapper`.
127+
128+
# Testing helpers
129+
130+
Two additional entry points help to test components that render Kaoto forms. They rely on [Vitest](https://vitest.dev/), so they are only intended for Vitest-based test suites.
131+
132+
```ts
133+
import { FieldTestProvider, FormWrapper } from '@kaoto/forms/testing';
134+
import { KaotoFormPageObject } from '@kaoto/forms/testing/page-object';
135+
```
136+
137+
# Development
138+
139+
This package lives in the [Kaoto monorepo](https://github.com/KaotoIO/kaoto) under `packages/forms`. It is currently excluded from the root Yarn workspaces (see the root `package.json`), so run the commands from this folder:
140+
141+
```bash
142+
npx vitest run --config vitest.config.mts # run the unit tests
143+
npx tsc --noEmit --project tsconfig.json # type-check the sources
144+
npx eslint "src/**/*.{ts,tsx}" # lint the sources
145+
npx rimraf dist && npx tsc --project tsconfig.lib.json \
146+
&& npx copyfiles -u 1 "./src/**/*.scss" ./dist # build the library into ./dist
147+
```
148+
149+
Dependencies are installed from the repository root with `yarn install`.

‎packages/forms/index.html‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/icon" href="/favicon.ico" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Kaoto forms</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/testapp/main.tsx"></script>
12+
</body>
13+
</html>

‎packages/forms/package.json‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
{
2+
"name": "@kaoto/forms",
3+
"version": "2.12.0-dev",
4+
"description": "Forms library for Kaoto",
5+
"license": "Apache-2.0",
6+
"author": {
7+
"name": "The Kaoto Team"
8+
},
9+
"publishConfig": {
10+
"access": "public"
11+
},
12+
"keywords": [],
13+
"homepage": "https://github.com/KaotoIO/forms",
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/KaotoIO/forms.git"
17+
},
18+
"bugs": {
19+
"url": "https://github.com/KaotoIO/forms/issues"
20+
},
21+
"types": "./dist/index.d.ts",
22+
"main": "dist/index.js",
23+
"exports": {
24+
".": {
25+
"source": "./src/index.ts",
26+
"default": "./dist/index.js"
27+
},
28+
"./testing": {
29+
"source": "./src/testing.ts",
30+
"default": "./dist/testing.js"
31+
},
32+
"./testing/page-object": {
33+
"source": "./src/testing/KaotoFormPageObject.ts",
34+
"default": "./dist/testing/KaotoFormPageObject.js"
35+
}
36+
},
37+
"files": [
38+
"dist"
39+
],
40+
"scripts": {
41+
"build": "yarn rimraf dist && yarn build:lib",
42+
"build:lib": "tsc --project tsconfig.lib.json && copyfiles -u 1 ./src/**/*.scss ./dist",
43+
"typecheck": "tsc --noEmit --project tsconfig.json",
44+
"lint": "eslint \"src/**/*.{ts,tsx}\"",
45+
"lint:fix": "yarn lint --fix",
46+
"test": "vitest run --config vitest.config.mts",
47+
"publish": "lerna publish"
48+
},
49+
"dependencies": {
50+
"ajv": "^8.12.0",
51+
"clsx": "^2.1.0",
52+
"lodash.clonedeep": "^4.5.0",
53+
"usehooks-ts": "^3.0.0"
54+
},
55+
"peerDependencies": {
56+
"@carbon/ibm-products": "^2.74.0",
57+
"@carbon/icons-react": "^11.67.0",
58+
"@carbon/react": "^1.102.0",
59+
"@carbon/styles": "^1.99.0",
60+
"react": "^19.2.4",
61+
"react-dom": "^19.2.4"
62+
},
63+
"devDependencies": {
64+
"@carbon/ibm-products": "^2.74.0",
65+
"@carbon/icons-react": "^11.67.0",
66+
"@carbon/react": "^1.102.0",
67+
"@carbon/styles": "^1.99.0",
68+
"@eslint/js": "^9.10.0",
69+
"@lerna-lite/cli": "^4.11.2",
70+
"@lerna-lite/publish": "^4.11.2",
71+
"@lerna-lite/version": "^4.11.2",
72+
"@patternfly/react-code-editor": "6.6.1",
73+
"@testing-library/dom": "^10.4.1",
74+
"@testing-library/jest-dom": "^6.9.1",
75+
"@testing-library/react": "^16.3.2",
76+
"@testing-library/user-event": "^14.6.1",
77+
"@types/json-schema": "^7.0.15",
78+
"@types/lodash": "^4",
79+
"@types/node": "^22.0.0",
80+
"@types/react": "^19.2.14",
81+
"@types/react-dom": "^19.2.3",
82+
"@vitejs/plugin-react": "^6.1.1",
83+
"@vitest/coverage-v8": "^5.0.1",
84+
"@vitest/eslint-plugin": "^1.6.27",
85+
"copyfiles": "^2.4.1",
86+
"cypress": "^14.5.0",
87+
"eslint": "^9.10.0",
88+
"eslint-config-prettier": "^9.1.0",
89+
"eslint-plugin-prettier": "^5.2.1",
90+
"eslint-plugin-react": "^7.35.2",
91+
"eslint-plugin-react-hooks": "5.1.0-rc-eb3ad065-20240822",
92+
"globals": "^15.9.0",
93+
"jsdom": "^26.1.0",
94+
"monaco-editor": "^0.55.1",
95+
"postcss": "^8.4.45",
96+
"prettier": "^3.0.0",
97+
"react": "^19.2.4",
98+
"react-dom": "^19.2.4",
99+
"react-is": "^19.0.0",
100+
"react-monaco-editor": "^0.59.0",
101+
"react-test-renderer": "^19.2.4",
102+
"rimraf": "^6.0.0",
103+
"sass": "npm:sass-embedded@^1.79.5",
104+
"sass-embedded": "^1.79.5",
105+
"stylelint": "^17.0.0",
106+
"stylelint-config-standard-scss": "^17.0.0",
107+
"stylelint-prettier": "^5.0.0",
108+
"ts-node": "^10.9.1",
109+
"typescript": "^6.0.0",
110+
"typescript-eslint": "^8.5.0",
111+
"vite": "^8.3.0",
112+
"vite-plugin-dts": "^5.1.1",
113+
"vite-plugin-static-copy": "^4.1.1",
114+
"vitest": "^5.0.1"
115+
},
116+
"packageManager": "yarn@4.13.0"
117+
}

‎packages/forms/public/.gitkeep‎

Whitespace-only changes.

‎packages/forms/public/favicon.ico‎

14.7 KB
Binary file not shown.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.no-field-found {
2+
&.cds--actionable-notification--low-contrast.cds--actionable-notification--info {
3+
background-color: var(--cds-field);
4+
}
5+
6+
.cds--actionable-notification__text-wrapper {
7+
flex-direction: column;
8+
}
9+
10+
.cds--actionable-notification__subtitle {
11+
padding-top: 0.25rem;
12+
}
13+
14+
.cds--actionable-notification__button-wrapper {
15+
align-self: flex-end;
16+
margin-left: 2.1rem;
17+
}
18+
19+
&.cds--actionable-notification--hide-close-button .cds--actionable-notification__action-button.cds--btn--ghost {
20+
margin-inline-start: 1.1rem;
21+
border: 1px solid currentColor;
22+
}
23+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { fireEvent, render, screen } from '@testing-library/react';
2+
3+
import { CanvasFormTabsContext, CanvasFormTabsContextResult } from '../providers';
4+
import { NoFieldFound } from './NoFieldFound';
5+
6+
describe('NoFieldFound Component', () => {
7+
it('should render the alert with the correct tab name', () => {
8+
const mockContextValue: CanvasFormTabsContextResult = {
9+
selectedTab: 'Required',
10+
setSelectedTab: vi.fn(),
11+
};
12+
13+
render(
14+
<CanvasFormTabsContext.Provider value={mockContextValue}>
15+
<NoFieldFound />
16+
</CanvasFormTabsContext.Provider>,
17+
);
18+
19+
expect(screen.getByTestId('no-field-found')).toBeInTheDocument();
20+
expect(screen.getByText('No Required fields found')).toBeInTheDocument();
21+
});
22+
23+
it('should not suggest switching tabs when the All tab is already selected', () => {
24+
const mockContextValue: CanvasFormTabsContextResult = {
25+
selectedTab: 'All',
26+
setSelectedTab: vi.fn(),
27+
};
28+
29+
render(
30+
<CanvasFormTabsContext.Provider value={mockContextValue}>
31+
<NoFieldFound />
32+
</CanvasFormTabsContext.Provider>,
33+
);
34+
35+
expect(screen.getByText('No fields found')).toBeInTheDocument();
36+
expect(screen.getByText('No field found matching this criteria.')).toBeInTheDocument();
37+
expect(screen.queryByRole('button', { name: 'Switch to All tab' })).not.toBeInTheDocument();
38+
});
39+
40+
it('should call onTabChange when the link is clicked', () => {
41+
const mockContextValue: CanvasFormTabsContextResult = {
42+
selectedTab: 'Required',
43+
setSelectedTab: vi.fn(),
44+
};
45+
46+
render(
47+
<CanvasFormTabsContext.Provider value={mockContextValue}>
48+
<NoFieldFound />
49+
</CanvasFormTabsContext.Provider>,
50+
);
51+
52+
const button = screen.getByRole('button', { name: 'Switch to All tab' });
53+
fireEvent.click(button);
54+
55+
expect(mockContextValue.setSelectedTab).toHaveBeenCalledWith('All');
56+
});
57+
});

0 commit comments

Comments
 (0)