diff --git a/docs/en/github-issue-schema-form.md b/docs/en/github-issue-schema-form.md new file mode 100644 index 0000000000..99aa2af266 --- /dev/null +++ b/docs/en/github-issue-schema-form.md @@ -0,0 +1,103 @@ +--- +title: GitHub Issue Template - Schema Form Feature Request +--- + + + +# GitHub Issue Template + +Use this template when filing the feature request issue on `apache/apisix-dashboard`. + +--- + +**Title:** `feat(plugin): JSON Schema-driven form renderer (SchemaForm) for plugin configuration` + +**Body:** + +## Description + +APISIX plugins ship JSON Schema definitions for their configuration. The Dashboard currently relies on a raw JSON/Monaco editor for plugin config. If the Dashboard can render plugin configuration forms directly from JSON Schema, developer experience improves significantly and manual UI maintenance per plugin is eliminated. + +## Motivation + +- 150+ plugins, each with unique config schemas — maintaining hand-coded forms is unsustainable. +- The Admin API already exposes full JSON Schema at `GET /apisix/admin/schema/plugins/{name}`. +- The new Dashboard (React 19 + Mantine 8 + react-hook-form + zod) has the right foundation but lacks a schema-to-form bridge. + +## Deliverables + +### Must-have (P0) + +1. Reusable `SchemaForm` component that accepts a JSON Schema object and renders a complete form. +2. Widget mapping for basic types: `string` → TextInput, `number`/`integer` → NumberInput, `boolean` → Switch, `object` → nested fieldset, `array` → repeatable field group. +3. `enum` support → Select dropdown. +4. `default` values pre-populated, `required` fields marked/enforced. +5. Basic constraints: `minimum`/`maximum`, `minLength`/`maxLength`, `pattern`, `exclusiveMinimum`, `minItems`/`maxItems`. +6. `oneOf` support — selector to choose a variant, render corresponding fields. +7. `dependencies` / conditional fields — show/hide fields based on other field values. +8. `if/then/else` conditional sub-schema rendering (used heavily: limit-conn policy→redis, jwt-auth algorithm→public_key). +9. Validation via AJV against the original JSON Schema, with inline error display. +10. `encrypt_fields` meta → render as password inputs. + +### Should-have (P1) + +11. `anyOf` support (select + render). +12. `patternProperties` support (dynamic key-value editor). +13. Fallback to Monaco JSON editor for unrecognized/complex schema portions. +14. Schema-to-widget override registry (custom widget for specific plugin fields). + +### Nice-to-have (P2) + +15. `allOf` merging. +16. `$ref` / `$defs` resolution. +17. Read-only/disabled mode for viewing existing config. + +## Non-goals + +- Schema authoring/editing in the UI. +- Modifying APISIX core Lua schema definitions. + +## Technical Notes + +- Schemas are fetched at runtime from Admin API; no build-time schema bundling needed. +- Existing `src/components/form/` widgets should be reused/wrapped. +- Validation: AJV for schema validation, bridge errors to react-hook-form `setError`. +- APISIX-specific non-standard keys (`encrypt_fields`, `_meta`) need explicit handling. + +## APISIX Plugin Schema Patterns to Support + +| Pattern | Example Plugins | +|---|---| +| `oneOf` (top-level required alternatives) | `limit-conn`, `limit-count` | +| `oneOf` (field-level type union) | `limit-conn` (`conn`) | +| `dependencies` + `oneOf` | `jwt-auth` consumer schema | +| `dependencies` + `not` (mutual exclusion) | `response-rewrite` | +| `if/then/else` | `limit-conn`, `openid-connect`, `ai-proxy` | +| `anyOf` | `schema_def.lua` id_schema, `ai-proxy-multi` | +| `enum` | Ubiquitous | +| `patternProperties` | `ai-proxy` auth_schema, `labels_def` | +| Nested `object` | Nearly all plugins | +| `array` of objects | `traffic-split`, health checker | + +## Related + +- Admin API schema endpoint: `apisix/admin/plugins.lua` +- Core schemas: `apisix/schema_def.lua` +- Dashboard technical design: https://github.com/apache/apisix-dashboard/issues/2988 diff --git a/docs/en/schema-form-developer-guide.md b/docs/en/schema-form-developer-guide.md new file mode 100644 index 0000000000..deb1605faa --- /dev/null +++ b/docs/en/schema-form-developer-guide.md @@ -0,0 +1,293 @@ +--- +title: Schema Form Developer Guide +--- + + + +# SchemaForm Developer Guide + +The `SchemaForm` component renders APISIX plugin configuration forms directly from JSON Schema definitions. This eliminates manual UI maintenance per plugin and ensures form UIs stay in sync with plugin schemas automatically. + +## Quick Start + +```tsx +import { SchemaForm } from '@/components/schema-form'; + +// Schema fetched from Admin API: GET /apisix/admin/schema/plugins/{name} +const pluginSchema = { + type: 'object', + properties: { + key: { type: 'string', minLength: 1 }, + timeout: { type: 'integer', minimum: 1, default: 3 }, + ssl_verify: { type: 'boolean', default: true }, + }, + required: ['key'], +}; + +function PluginConfigForm() { + return ( + console.log('Valid config:', data)} + onChange={(data) => console.log('Changed:', data)} + /> + ); +} +``` + +## Architecture + +``` +SchemaForm (entry point, provides react-hook-form context) + └── SchemaField (dispatcher — routes to correct field component) + ├── StringField → Mantine TextInput / PasswordInput / Textarea + ├── NumberField → Mantine NumberInput + ├── BooleanField → Mantine Switch + ├── EnumField → Mantine Select + ├── ObjectField → Mantine Fieldset (recursive) + │ ├── ConditionalProperties (if/then/else) + │ └── DependencyProperties (dependencies) + ├── ArrayField → TagsInput (primitives) or repeatable Fieldset (objects) + ├── OneOfField → SegmentedControl + variant fields + ├── AnyOfField → SegmentedControl + variant fields + └── PatternPropertiesField → dynamic key-value editor +``` + +### Data Flow + +1. **Schema** is passed as a prop (fetched from APISIX Admin API at runtime). +2. `SchemaForm` initializes `react-hook-form` with defaults resolved from the schema. +3. `SchemaField` dispatches to the correct field component based on schema type/features. +4. Field components use `useController` to bind to react-hook-form state. +5. On submit, **AJV** validates the full form data against the original JSON Schema. +6. Validation errors are mapped back to individual form fields. + +## Supported JSON Schema Features + +### Basic Types + +| Schema Type | Widget | Notes | +|---|---|---| +| `string` | `TextInput` | Auto-uses `Textarea` for `maxLength > 256` | +| `string` + `encrypt_fields` | `PasswordInput` | Masked input with reveal toggle | +| `string` + `enum` | `Select` | Dropdown with enum values | +| `number` / `integer` | `NumberInput` | Respects `min`/`max`/`step` | +| `boolean` | `Switch` | Left-positioned label | +| `object` | `Fieldset` | Recursive rendering of properties | +| `array` of strings | `TagsInput` | Tag-style input | +| `array` of objects | Repeatable `Fieldset` | Add/remove items | + +### Constraints + +All standard JSON Schema constraints are supported: + +- `required` — field marked as required, validated on submit +- `default` — pre-populated in the form +- `minimum` / `maximum` / `exclusiveMinimum` / `exclusiveMaximum` +- `minLength` / `maxLength` +- `pattern` — shown as placeholder hint +- `minItems` / `maxItems` — controls add/remove in arrays +- `enum` — rendered as Select dropdown + +### Composition Keywords + +#### `oneOf` + +Used in two patterns in APISIX: + +1. **Top-level required alternatives** (e.g., limit-conn: `conn+burst+key` vs `rules`): + Renders a `SegmentedControl` to select the configuration mode. + +2. **Field-level type unions** (e.g., `conn: oneOf [{integer}, {string}]`): + Renders the primary type (first option). The schema allows either an integer value or a string variable reference. + +#### `anyOf` + +Similar to `oneOf` but allows matching multiple schemas. Rendered with a `SegmentedControl` selector. + +#### `if / then / else` + +Conditional sub-schemas that show/hide fields based on other field values. Common APISIX pattern: + +```json +{ + "if": { "properties": { "policy": { "enum": ["redis"] } } }, + "then": { "properties": { "redis_host": { "type": "string" } }, "required": ["redis_host"] }, + "else": {} +} +``` + +The form watches the `policy` field and dynamically shows `redis_host` when `policy === "redis"`. + +Supports **nested** if/then/else chains (e.g., limit-conn: redis → redis-cluster fallback). + +#### `dependencies` + +Two forms: + +1. **Simple dependencies** (`dependencies: { "a": ["b"] }`): If `a` is present, `b` is required. Handled by AJV validation. + +2. **Schema dependencies with `oneOf`** (e.g., jwt-auth consumer schema): + ```json + { + "dependencies": { + "algorithm": { + "oneOf": [ + { "properties": { "algorithm": { "enum": ["HS256", "HS384"] } } }, + { "properties": { "public_key": { "type": "string" } }, "required": ["public_key"] } + ] + } + } + } + ``` + Watches the `algorithm` field and shows `public_key` when a non-HMAC algorithm is selected. + +### APISIX-Specific Features + +- **`encrypt_fields`**: Fields listed here are rendered as `PasswordInput` (masked). +- **`_meta`**: The plugin injected meta schema is stripped during validation. + +## Validation + +Validation uses [AJV](https://ajv.js.org/) (JSON Schema draft-07) configured with: + +- `allErrors: true` — collect all errors, not just the first +- `strict: false` — allow APISIX-specific keywords +- `ajv-formats` — support for `format: "ipv4"`, `format: "ipv6"`, etc. + +APISIX-specific fields (`encrypt_fields`, `_meta`, `$comment`) are stripped before compilation. + +### Error Mapping + +AJV errors are mapped to react-hook-form field paths: + +``` +/properties/redis_host → redis_host +/items/0/host → items.0.host +``` + +Missing required fields include the field name in the path for precise error placement. + +## How to Extend + +### Adding a Custom Widget + +To render a specific field with a custom component: + +1. Create your widget component implementing the `FieldProps` interface: + +```tsx +import type { FieldProps } from '@/components/schema-form/types'; + +export const MyCustomWidget: React.FC = ({ schema, name, required }) => { + // Use useController from react-hook-form to bind to form state + const { control } = useFormContext(); + const { field, fieldState } = useController({ name, control }); + + return
Custom rendering for {name}
; +}; +``` + +2. Modify `SchemaField.tsx` to route to your widget for the specific case: + +```tsx +// In SchemaField.tsx, add before the switch statement: +if (name === 'myPlugin.specialField') { + return ; +} +``` + +### Adding Support for a New Schema Pattern + +1. Identify the pattern in APISIX plugin schemas (grep for the keyword). +2. Create a new field component in `src/components/schema-form/fields/`. +3. Add routing logic in `SchemaField.tsx`. +4. Add unit tests in `__tests__/`. +5. Test with the actual plugin schema. + +### Handling Unknown Schema Fragments + +If the schema contains patterns that `SchemaField` cannot render, it displays a "Unsupported schema type" message. To add a fallback to the Monaco JSON editor: + +```tsx +import { Editor } from '@/components/form/Editor'; + +// In SchemaField.tsx default case: +default: + return ; +``` + +## Testing + +Run unit tests: + +```bash +pnpm test +``` + +Run in watch mode: + +```bash +pnpm test:watch +``` + +### Test Structure + +- `__tests__/utils.test.ts` — 41 tests for utility functions +- `__tests__/validation.test.ts` — 9 tests for AJV validation pipeline +- `__tests__/SchemaForm.test.tsx` — 10 integration tests for component rendering + +### Testing with Real Plugin Schemas + +To test with actual APISIX plugin schemas, fetch them from the Admin API: + +```bash +curl http://localhost:9180/apisix/admin/schema/plugins/limit-count \ + -H 'X-API-KEY: your-api-key' +``` + +Then pass the response JSON to `SchemaForm` as the `schema` prop. + +## File Structure + +``` +src/components/schema-form/ +├── index.ts # Barrel exports +├── SchemaForm.tsx # Entry point component +├── types.ts # TypeScript type definitions +├── utils.ts # Schema helper utilities +├── validation.ts # AJV validation bridge +├── fields/ +│ ├── SchemaField.tsx # Field dispatcher +│ ├── StringField.tsx # String input +│ ├── NumberField.tsx # Number/integer input +│ ├── BooleanField.tsx # Switch toggle +│ ├── EnumField.tsx # Select dropdown +│ ├── ObjectField.tsx # Nested object fieldset +│ ├── ArrayField.tsx # Array (tags or repeatable) +│ ├── OneOfField.tsx # oneOf selector +│ ├── AnyOfField.tsx # anyOf selector +│ └── PatternPropertiesField.tsx # Dynamic key-value editor +└── __tests__/ + ├── utils.test.ts + ├── validation.test.ts + └── SchemaForm.test.tsx +``` diff --git a/package.json b/package.json index ff6720917a..1f05dfaf78 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "lint:fix": "pnpm lint --fix", "preview": "vite preview", "prepare": "husky", - "e2e": "playwright test" + "e2e": "playwright test", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@ant-design/pro-components": "^2.8.7", @@ -23,6 +25,8 @@ "@monaco-editor/react": "^4.7.0", "@tanstack/react-query": "^5.74.4", "@tanstack/react-router": "^1.116.0", + "ajv": "^8.18.0", + "ajv-formats": "^3.0.1", "antd": "^5.24.8", "axios": "^1.13.2", "clsx": "^2.1.1", @@ -61,6 +65,9 @@ "@tanstack/react-query-devtools": "^5.74.4", "@tanstack/react-router-devtools": "^1.116.0", "@tanstack/router-plugin": "^1.116.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^22.14.1", "@types/qs": "^6.9.18", "@types/react": "^19.1.3", @@ -80,6 +87,7 @@ "globals": "^16.0.0", "husky": "^9.1.7", "jiti": "^2.4.2", + "jsdom": "^28.1.0", "lint-staged": "^15.5.2", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", @@ -90,6 +98,7 @@ "unplugin-info": "^1.2.2", "vite": "^6.3.6", "vite-tsconfig-paths": "^5.1.4", + "vitest": "^4.0.18", "yaml": "^2.7.1" }, "lint-staged": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 518d990b5b..69cbcb11f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,6 +43,12 @@ importers: '@tanstack/react-router': specifier: ^1.116.0 version: 1.147.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + ajv: + specifier: ^8.18.0 + version: 8.18.0 + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.18.0) antd: specifier: ^5.24.8 version: 5.29.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -152,6 +158,15 @@ importers: '@tanstack/router-plugin': specifier: ^1.116.1 version: 1.149.0(@tanstack/react-router@1.147.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.4.1(@types/node@22.19.5)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(tsx@4.21.0)(yaml@2.8.2)) + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) '@types/node': specifier: ^22.14.1 version: 22.19.5 @@ -209,6 +224,9 @@ importers: jiti: specifier: ^2.4.2 version: 2.6.1 + jsdom: + specifier: ^28.1.0 + version: 28.1.0 lint-staged: specifier: ^15.5.2 version: 15.5.2 @@ -239,12 +257,18 @@ importers: vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.5)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(tsx@4.21.0)(yaml@2.8.2)) + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@22.19.5)(jiti@2.6.1)(jsdom@28.1.0)(sugarss@5.0.1(postcss@8.5.6))(tsx@4.21.0)(yaml@2.8.2) yaml: specifier: ^2.7.1 version: 2.8.2 packages: + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + '@actions/core@1.11.1': resolution: {integrity: sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==} @@ -257,6 +281,9 @@ packages: '@actions/io@1.1.3': resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==} + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@ant-design/colors@7.2.1': resolution: {integrity: sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==} @@ -378,6 +405,16 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@asamuzakjp/css-color@5.0.1': + resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -465,11 +502,46 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@chenshuai2144/sketch-color@1.0.9': resolution: {integrity: sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==} peerDependencies: react: '>=16.12.0' + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.1.1': + resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.0.2': + resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.0.28': + resolution: {integrity: sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==} + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@ctrl/tinycolor@3.6.1': resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} engines: {node: '>=10'} @@ -926,6 +998,15 @@ packages: peerDependencies: '@playwright/test': ^1.42.1 + '@exodus/bytes@1.14.1': + resolution: {integrity: sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} @@ -1289,6 +1370,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -1531,9 +1615,47 @@ packages: resolution: {integrity: sha512-CI75JrfqSluhdGwLssgVeQBaCphgfkMQpi8MCY3UJX1hoGzXa8kHYJcUuIFMOLs1q7zqHy++EVVtMK03osR5wQ==} engines: {node: '>=12'} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1737,6 +1859,35 @@ packages: peerDependencies: vite: ^4 || ^5 || ^6 || ^7 + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@xobotyi/scrollbar-width@1.9.5': resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} @@ -1753,13 +1904,32 @@ packages: add-dom-event-listener@1.1.0: resolution: {integrity: sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-escapes@7.2.0: resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} engines: {node: '>=18'} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} @@ -1768,6 +1938,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -1794,6 +1968,13 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -1830,6 +2011,10 @@ packages: resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==} engines: {node: '>=12.0.0'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} @@ -1865,6 +2050,9 @@ packages: resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} hasBin: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1915,6 +2103,10 @@ packages: caniuse-lite@1.0.30001764: resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2012,14 +2204,29 @@ packages: resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} engines: {node: '>=8.0.0'} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + cssstyle@6.1.0: + resolution: {integrity: sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==} + engines: {node: '>=20'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2052,6 +2259,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2082,6 +2292,12 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -2109,6 +2325,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -2135,6 +2355,9 @@ packages: resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2315,6 +2538,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2326,6 +2552,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -2345,6 +2575,9 @@ packages: fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastest-stable-stringify@2.0.2: resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} @@ -2519,9 +2752,21 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -2561,6 +2806,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inline-style-prefixer@7.0.1: resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} @@ -2650,6 +2899,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2739,6 +2991,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@28.1.0: + resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2753,6 +3014,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2831,9 +3095,20 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked@12.0.2: resolution: {integrity: sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==} engines: {node: '>= 18'} @@ -2846,6 +3121,9 @@ packages: mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2869,6 +3147,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2982,6 +3264,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} @@ -3024,6 +3309,9 @@ packages: resolution: {integrity: sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==} engines: {node: '>=14.13.0'} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3136,6 +3424,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -3433,6 +3725,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -3521,6 +3816,10 @@ packages: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -3532,6 +3831,10 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + requireindex@1.1.0: resolution: {integrity: sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==} engines: {node: '>=0.10.5'} @@ -3586,6 +3889,10 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -3662,6 +3969,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -3707,6 +4017,9 @@ packages: stack-generator@2.0.10: resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -3719,6 +4032,9 @@ packages: state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -3765,6 +4081,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -3800,6 +4120,9 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -3817,6 +4140,9 @@ packages: tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} @@ -3828,6 +4154,17 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + + tldts-core@7.0.23: + resolution: {integrity: sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==} + + tldts@7.0.23: + resolution: {integrity: sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3835,6 +4172,14 @@ packages: toggle-selection@1.0.6: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -3926,6 +4271,10 @@ packages: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} + engines: {node: '>=20.18.1'} + unplugin-icons@22.5.0: resolution: {integrity: sha512-MBlMtT5RuMYZy4TZgqUL2OTtOdTUVsS1Mhj6G1pEzMlFJlEnq6mhUfoIt45gBWxHcsOdXJDWLg3pRZ+YmvAVWQ==} peerDependencies: @@ -4101,16 +4450,66 @@ packages: yaml: optional: true + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -4132,6 +4531,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4140,6 +4544,13 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4173,6 +4584,8 @@ packages: snapshots: + '@acemir/cssom@0.9.31': {} + '@actions/core@1.11.1': dependencies: '@actions/exec': 1.1.1 @@ -4189,6 +4602,8 @@ snapshots: '@actions/io@1.1.3': {} + '@adobe/css-tools@4.4.4': {} + '@ant-design/colors@7.2.1': dependencies: '@ant-design/fast-color': 2.0.6 @@ -4437,6 +4852,24 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.2 + '@asamuzakjp/css-color@5.0.1': + dependencies: + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.2.6 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.6 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4551,12 +4984,38 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.1.0 + '@chenshuai2144/sketch-color@1.0.9(react@19.1.0)': dependencies: react: 19.1.0 reactcss: 1.2.3(react@19.1.0) tinycolor2: 1.6.0 + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.0.28': {} + + '@csstools/css-tokenizer@4.0.0': {} + '@ctrl/tinycolor@3.6.1': {} '@dnd-kit/accessibility@3.1.1(react@19.1.0)': @@ -4903,6 +5362,8 @@ snapshots: ansi-to-html: 0.7.2 marked: 12.0.2 + '@exodus/bytes@1.14.1': {} + '@fastify/busboy@2.1.1': {} '@floating-ui/core@1.7.3': @@ -5305,6 +5766,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': @@ -5545,11 +6008,54 @@ snapshots: '@tanstack/virtual-file-routes@1.145.4': {} + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.4 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@testing-library/dom': 10.4.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/js-cookie@2.2.7': {} @@ -5744,6 +6250,45 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.18(vite@6.4.1(@types/node@22.19.5)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@22.19.5)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(tsx@4.21.0)(yaml@2.8.2) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + '@xobotyi/scrollbar-width@1.9.5': {} acorn-jsx@5.3.2(acorn@8.15.0): @@ -5756,6 +6301,12 @@ snapshots: dependencies: object-assign: 4.1.1 + agent-base@7.1.4: {} + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -5763,16 +6314,27 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-escapes@7.2.0: dependencies: environment: 1.1.0 + ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} ansi-to-html@0.7.2: @@ -5846,6 +6408,12 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -5919,6 +6487,8 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 + assertion-error@2.0.1: {} + ast-types@0.16.1: dependencies: tslib: 2.8.1 @@ -5966,6 +6536,10 @@ snapshots: baseline-browser-mapping@2.9.14: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} brace-expansion@1.1.12: @@ -6016,6 +6590,8 @@ snapshots: caniuse-lite@1.0.30001764: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6114,10 +6690,31 @@ snapshots: mdn-data: 2.0.14 source-map: 0.6.1 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + cssesc@3.0.0: {} + cssstyle@6.1.0: + dependencies: + '@asamuzakjp/css-color': 5.0.1 + '@csstools/css-syntax-patches-for-csstree': 1.0.28 + css-tree: 3.1.0 + lru-cache: 11.2.6 + csstype@3.2.3: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -6146,6 +6743,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -6172,6 +6771,10 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.28.4 @@ -6198,6 +6801,8 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: {} + environment@1.1.0: {} error-ex@1.3.4: @@ -6288,6 +6893,8 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -6563,6 +7170,10 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} eventemitter3@5.0.1: {} @@ -6579,6 +7190,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + expect-type@1.3.0: {} + exsolve@1.0.8: {} fast-clean@1.5.3: {} @@ -6591,6 +7204,8 @@ snapshots: fast-shallow-equal@1.0.0: {} + fast-uri@3.1.0: {} + fastest-stable-stringify@2.0.2: {} fdir@6.5.0(picomatch@4.0.3): @@ -6750,10 +7365,30 @@ snapshots: dependencies: react-is: 16.13.1 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.14.1 + transitivePeerDependencies: + - '@noble/hashes' + html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@5.0.0: {} husky@9.1.7: {} @@ -6779,6 +7414,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inline-style-prefixer@7.0.1: dependencies: css-in-js-utils: 3.1.0 @@ -6874,6 +7511,8 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -6951,6 +7590,33 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@28.1.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@bramus/specificity': 2.4.2 + '@exodus/bytes': 1.14.1 + cssstyle: 6.1.0 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + undici: 7.22.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - supports-color + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -6959,6 +7625,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json2mq@0.2.0: @@ -7053,16 +7721,26 @@ snapshots: dependencies: tslib: 2.8.1 + lru-cache@11.2.6: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + marked@12.0.2: {} math-intrinsics@1.1.0: {} mdn-data@2.0.14: {} + mdn-data@2.12.2: {} + merge-stream@2.0.0: {} micromatch@4.0.8: @@ -7080,6 +7758,8 @@ snapshots: mimic-function@5.0.1: {} + min-indent@1.0.1: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -7203,6 +7883,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -7256,6 +7938,10 @@ snapshots: '@types/parse-path': 7.1.0 parse-path: 7.1.0 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -7352,6 +8038,12 @@ snapshots: prettier@3.7.4: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -7735,6 +8427,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@18.3.1: {} react-lifecycles-compat@3.0.4: {} @@ -7836,6 +8530,11 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + reflect-metadata@0.2.2: {} reflect.getprototypeof@1.0.10: @@ -7858,6 +8557,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + require-from-string@2.0.2: {} + requireindex@1.1.0: {} resize-observer-polyfill@1.5.1: {} @@ -7941,6 +8642,10 @@ snapshots: safe-stable-stringify@2.5.0: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.26.0: {} screenfull@5.2.0: {} @@ -8024,6 +8729,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} simple-git@3.30.0: @@ -8065,6 +8772,8 @@ snapshots: dependencies: stackframe: 1.3.4 + stackback@0.0.2: {} + stackframe@1.3.4: {} stacktrace-gps@3.1.2: @@ -8080,6 +8789,8 @@ snapshots: state-local@1.0.7: {} + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -8147,6 +8858,10 @@ snapshots: strip-final-newline@3.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-json-comments@3.1.1: {} stylis@4.2.0: {} @@ -8173,6 +8888,8 @@ snapshots: react: 19.1.0 use-sync-external-store: 1.6.0(react@19.1.0) + symbol-tree@3.2.4: {} + tabbable@6.4.0: {} throttle-debounce@3.0.1: {} @@ -8183,6 +8900,8 @@ snapshots: tiny-warning@1.0.3: {} + tinybench@2.9.0: {} + tinycolor2@1.6.0: {} tinyexec@1.0.2: {} @@ -8192,12 +8911,28 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.0.3: {} + + tldts-core@7.0.23: {} + + tldts@7.0.23: + dependencies: + tldts-core: 7.0.23 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 toggle-selection@1.0.6: {} + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.23 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.4.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -8299,6 +9034,8 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 + undici@7.22.0: {} + unplugin-icons@22.5.0(@svgr/core@8.1.0(typescript@5.8.3)): dependencies: '@antfu/install-pkg': 1.1.0 @@ -8440,14 +9177,68 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 + vitest@4.0.18(@types/node@22.19.5)(jiti@2.6.1)(jsdom@28.1.0)(sugarss@5.0.1(postcss@8.5.6))(tsx@4.21.0)(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@6.4.1(@types/node@22.19.5)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 6.4.1(@types/node@22.19.5)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.5 + jsdom: 28.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + void-elements@3.1.0: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + warning@4.0.3: dependencies: loose-envify: 1.4.0 + webidl-conversions@8.0.1: {} + webpack-virtual-modules@0.6.2: {} + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.14.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -8493,6 +9284,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@9.0.2: @@ -8501,6 +9297,10 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + yallist@3.1.1: {} yaml@1.10.2: {} diff --git a/src/components/schema-form/SchemaForm.tsx b/src/components/schema-form/SchemaForm.tsx new file mode 100644 index 0000000000..11e60ab719 --- /dev/null +++ b/src/components/schema-form/SchemaForm.tsx @@ -0,0 +1,176 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Alert, Box, Button, Group, Stack } from '@mantine/core'; +import { useCallback, useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { SchemaField } from './fields/SchemaField'; +import type { JSONSchema, SchemaFormProps } from './types'; +import { resolveDefaults } from './utils'; +import { + validateAgainstSchema, + type ValidationError, +} from './validation'; + +export const SchemaForm: React.FC = ({ + schema, + value, + onChange, + onSubmit, + encryptFields, + disabled, + rootPath = '', +}) => { + const defaults = resolveDefaults(schema); + const initialValues = { ...defaults, ...value }; + const [validationErrors, setValidationErrors] = useState( + [] + ); + + const methods = useForm({ + defaultValues: initialValues, + mode: 'onBlur', + }); + + const handleChange = useCallback( + (data: Record) => { + onChange?.(data); + }, + [onChange] + ); + + const handleSubmit = useCallback( + (data: Record) => { + // Run AJV validation against the full schema + const errors = validateAgainstSchema(schema, data); + setValidationErrors(errors); + + if (errors.length > 0) { + // Map errors back to form fields + for (const err of errors) { + if (err.path) { + methods.setError(err.path, { + type: 'validate', + message: err.message, + }); + } + } + return; + } + + onSubmit?.(data); + }, + [schema, onSubmit, methods] + ); + + // Watch for changes and propagate + if (onChange) { + methods.watch((data) => { + handleChange(data as Record); + }); + } + + if (!schema || !schema.properties) { + return ( + + No renderable schema properties found. + + ); + } + + const requiredSet = new Set(schema.required ?? []); + + return ( + +
+ + {Object.entries(schema.properties).map(([key, propSchema]) => ( + + + + ))} + + + + {validationErrors.length > 0 && ( + +
    + {validationErrors.map((err, i) => ( +
  • + {err.path ? {err.path}: : null}{' '} + {err.message} +
  • + ))} +
+
+ )} + + {onSubmit && ( + + + + )} +
+
+
+ ); +}; + +const ConditionalTopLevel: React.FC<{ + schema: JSONSchema; + encryptFields?: string[]; + disabled?: boolean; + rootPath: string; +}> = ({ schema, encryptFields, disabled, rootPath }) => { + // Handle top-level oneOf (required-field alternatives) + // These are typically just mode selectors and don't add extra properties + // The actual rendering is handled by individual field components + + // Handle top-level if/then/else + if (schema.if && (schema.then || schema.else)) { + return ( + + ); + } + + return null; +}; diff --git a/src/components/schema-form/__tests__/SchemaForm.test.tsx b/src/components/schema-form/__tests__/SchemaForm.test.tsx new file mode 100644 index 0000000000..ededed2206 --- /dev/null +++ b/src/components/schema-form/__tests__/SchemaForm.test.tsx @@ -0,0 +1,164 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MantineProvider } from '@mantine/core'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { SchemaForm } from '../SchemaForm'; +import type { JSONSchema } from '../types'; + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe('SchemaForm', () => { + it('renders string fields', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + key: { type: 'string', description: 'API key' }, + }, + required: ['key'], + }; + + render(, { wrapper }); + expect(screen.getByLabelText(/key/i)).toBeInTheDocument(); + }); + + it('renders number fields', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + timeout: { type: 'integer', minimum: 1, default: 3 }, + }, + }; + + render(, { wrapper }); + expect(screen.getByLabelText(/timeout/i)).toBeInTheDocument(); + }); + + it('renders boolean fields', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + ssl_verify: { type: 'boolean', default: true }, + }, + }; + + render(, { wrapper }); + expect(screen.getByLabelText(/ssl verify/i)).toBeInTheDocument(); + }); + + it('renders enum fields as select', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + policy: { + type: 'string', + enum: ['local', 'redis', 'redis-cluster'], + default: 'local', + }, + }, + }; + + render(, { wrapper }); + // Mantine Select renders label text + a combobox input + expect(screen.getByText('Policy')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('renders nested object fields', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + timeout: { + type: 'object', + properties: { + connect: { type: 'number' }, + send: { type: 'number' }, + read: { type: 'number' }, + }, + required: ['connect', 'send', 'read'], + }, + }, + }; + + render(, { wrapper }); + expect(screen.getByText(/timeout/i)).toBeInTheDocument(); + }); + + it('shows alert for empty schema', () => { + render(, { wrapper }); + expect(screen.getByText(/no renderable schema/i)).toBeInTheDocument(); + }); + + it('renders a key-auth-like schema', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + header: { type: 'string', default: 'apikey' }, + query: { type: 'string', default: 'apikey' }, + hide_credentials: { type: 'boolean', default: false }, + }, + }; + + render(, { wrapper }); + expect(screen.getByLabelText(/header/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/query/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/hide credentials/i)).toBeInTheDocument(); + }); + + it('renders password input for encrypt fields', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + client_id: { type: 'string' }, + client_secret: { type: 'string' }, + }, + encrypt_fields: ['client_secret'], + }; + + render(, { wrapper }); + expect(screen.getByLabelText(/client id/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/client secret/i)).toBeInTheDocument(); + }); + + it('renders submit button when onSubmit is provided', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + key: { type: 'string' }, + }, + }; + + render( {}} />, { wrapper }); + expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument(); + }); + + it('does not render submit button when onSubmit is not provided', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + key: { type: 'string' }, + }, + }; + + render(, { wrapper }); + expect(screen.queryByRole('button', { name: /submit/i })).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/schema-form/__tests__/utils.test.ts b/src/components/schema-form/__tests__/utils.test.ts new file mode 100644 index 0000000000..bce1d2ca5c --- /dev/null +++ b/src/components/schema-form/__tests__/utils.test.ts @@ -0,0 +1,287 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; + +import type { JSONSchema } from '../types'; +import { + evaluateIfCondition, + fieldPath, + getFieldLabel, + getOneOfLabel, + getOneOfMatchIndex, + getRequiredSet, + getSchemaType, + hasConditionals, + hasDependencies, + isEncryptField, + isEnumField, + isRequired, + mergeSchemas, + resolveDefaults, +} from '../utils'; + +describe('getSchemaType', () => { + it('returns the type string directly', () => { + expect(getSchemaType({ type: 'string' })).toBe('string'); + expect(getSchemaType({ type: 'integer' })).toBe('integer'); + expect(getSchemaType({ type: 'boolean' })).toBe('boolean'); + }); + + it('returns first type from array', () => { + expect(getSchemaType({ type: ['string', 'null'] })).toBe('string'); + }); + + it('infers type from enum', () => { + expect(getSchemaType({ enum: ['a', 'b'] })).toBe('string'); + }); + + it('infers object from properties', () => { + expect(getSchemaType({ properties: { foo: { type: 'string' } } })).toBe('object'); + }); + + it('infers array from items', () => { + expect(getSchemaType({ items: { type: 'string' } })).toBe('array'); + }); + + it('infers oneOf', () => { + expect(getSchemaType({ oneOf: [{ type: 'string' }] })).toBe('oneOf'); + }); + + it('returns undefined for empty schema', () => { + expect(getSchemaType({})).toBeUndefined(); + }); +}); + +describe('isEnumField', () => { + it('returns true for schema with enum', () => { + expect(isEnumField({ enum: ['a', 'b', 'c'] })).toBe(true); + }); + + it('returns false for schema without enum', () => { + expect(isEnumField({ type: 'string' })).toBe(false); + }); + + it('returns false for empty enum', () => { + expect(isEnumField({ enum: [] })).toBe(false); + }); +}); + +describe('isEncryptField', () => { + it('returns true when field name is in encrypt list', () => { + expect(isEncryptField('client_secret', ['client_secret', 'client_key'])).toBe(true); + }); + + it('handles dotted paths by checking last segment', () => { + expect(isEncryptField('auth.client_secret', ['client_secret'])).toBe(true); + }); + + it('returns false when not in list', () => { + expect(isEncryptField('client_id', ['client_secret'])).toBe(false); + }); + + it('returns false for undefined list', () => { + expect(isEncryptField('secret', undefined)).toBe(false); + }); +}); + +describe('isRequired', () => { + it('returns true when field is in required array', () => { + const schema: JSONSchema = { type: 'object', required: ['name', 'key'] }; + expect(isRequired('name', schema)).toBe(true); + }); + + it('returns false when field is not in required array', () => { + const schema: JSONSchema = { type: 'object', required: ['name'] }; + expect(isRequired('key', schema)).toBe(false); + }); + + it('returns false when no required array', () => { + expect(isRequired('name', { type: 'object' })).toBe(false); + }); +}); + +describe('getRequiredSet', () => { + it('returns a Set of required fields', () => { + const schema: JSONSchema = { required: ['a', 'b'] }; + const set = getRequiredSet(schema); + expect(set.has('a')).toBe(true); + expect(set.has('b')).toBe(true); + expect(set.has('c')).toBe(false); + }); + + it('returns empty Set for no required', () => { + expect(getRequiredSet({}).size).toBe(0); + }); +}); + +describe('resolveDefaults', () => { + it('extracts defaults from properties', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + policy: { type: 'string', default: 'local' }, + timeout: { type: 'integer', default: 3 }, + name: { type: 'string' }, + }, + }; + const defaults = resolveDefaults(schema); + expect(defaults).toEqual({ policy: 'local', timeout: 3 }); + }); + + it('handles nested objects', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + session: { + type: 'object', + properties: { + storage: { type: 'string', default: 'cookie' }, + }, + }, + }, + }; + const defaults = resolveDefaults(schema); + expect(defaults).toEqual({ session: { storage: 'cookie' } }); + }); + + it('returns empty for non-object schemas', () => { + expect(resolveDefaults({ type: 'string' })).toEqual({}); + }); +}); + +describe('fieldPath', () => { + it('combines root and field name', () => { + expect(fieldPath('config', 'timeout')).toBe('config.timeout'); + }); + + it('returns field name when root is empty', () => { + expect(fieldPath('', 'timeout')).toBe('timeout'); + }); +}); + +describe('getFieldLabel', () => { + it('uses schema title when available', () => { + expect(getFieldLabel('foo', { title: 'My Field' })).toBe('My Field'); + }); + + it('generates label from field name', () => { + expect(getFieldLabel('redis_host', {})).toBe('Redis host'); + }); + + it('handles camelCase', () => { + expect(getFieldLabel('clientSecret', {})).toBe('Client Secret'); + }); + + it('handles dotted paths', () => { + expect(getFieldLabel('config.redis_host', {})).toBe('Redis host'); + }); +}); + +describe('hasConditionals', () => { + it('returns true for if/then', () => { + expect(hasConditionals({ if: { properties: {} }, then: {} })).toBe(true); + }); + + it('returns false for no conditionals', () => { + expect(hasConditionals({ type: 'object' })).toBe(false); + }); +}); + +describe('hasDependencies', () => { + it('returns true when dependencies exist', () => { + expect(hasDependencies({ dependencies: { a: ['b'] } })).toBe(true); + }); + + it('returns false for empty', () => { + expect(hasDependencies({})).toBe(false); + }); +}); + +describe('evaluateIfCondition', () => { + it('returns then when enum matches', () => { + const schema: JSONSchema = { + if: { properties: { policy: { enum: ['redis'] } } }, + then: { required: ['redis_host'] }, + else: {}, + }; + expect(evaluateIfCondition(schema, { policy: 'redis' })).toBe('then'); + }); + + it('returns else when enum does not match', () => { + const schema: JSONSchema = { + if: { properties: { policy: { enum: ['redis'] } } }, + then: { required: ['redis_host'] }, + else: {}, + }; + expect(evaluateIfCondition(schema, { policy: 'local' })).toBe('else'); + }); + + it('returns null when no if condition', () => { + expect(evaluateIfCondition({}, {})).toBeNull(); + }); +}); + +describe('mergeSchemas', () => { + it('merges properties from two schemas', () => { + const base: JSONSchema = { + type: 'object', + properties: { a: { type: 'string' } }, + }; + const ext: JSONSchema = { + properties: { b: { type: 'integer' } }, + required: ['b'], + }; + const merged = mergeSchemas(base, ext); + expect(merged.properties?.a).toBeDefined(); + expect(merged.properties?.b).toBeDefined(); + expect(merged.required).toContain('b'); + }); +}); + +describe('getOneOfMatchIndex', () => { + it('matches by required fields', () => { + const schemas: JSONSchema[] = [ + { required: ['conn', 'burst'] }, + { required: ['rules'] }, + ]; + expect(getOneOfMatchIndex(schemas, { conn: 10, burst: 5 })).toBe(0); + expect(getOneOfMatchIndex(schemas, { rules: [{}] })).toBe(1); + }); + + it('defaults to 0 when no match', () => { + const schemas: JSONSchema[] = [ + { required: ['a'] }, + { required: ['b'] }, + ]; + expect(getOneOfMatchIndex(schemas, {})).toBe(0); + }); +}); + +describe('getOneOfLabel', () => { + it('uses title when available', () => { + expect(getOneOfLabel({ title: 'Redis Mode' }, 0)).toBe('Redis Mode'); + }); + + it('uses required fields as label', () => { + expect(getOneOfLabel({ required: ['conn', 'burst'] }, 0)).toBe('conn + burst'); + }); + + it('falls back to Option N', () => { + expect(getOneOfLabel({}, 2)).toBe('Option 3'); + }); +}); diff --git a/src/components/schema-form/__tests__/validation.test.ts b/src/components/schema-form/__tests__/validation.test.ts new file mode 100644 index 0000000000..9c5b1c586a --- /dev/null +++ b/src/components/schema-form/__tests__/validation.test.ts @@ -0,0 +1,149 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; + +import type { JSONSchema } from '../types'; +import { + validateAgainstSchema, + validationErrorsToFieldErrors, +} from '../validation'; + +describe('validateAgainstSchema', () => { + it('returns no errors for valid data', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + key: { type: 'string', minLength: 1 }, + }, + required: ['key'], + }; + const errors = validateAgainstSchema(schema, { key: 'my-key' }); + expect(errors).toHaveLength(0); + }); + + it('returns error for missing required field', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + key: { type: 'string' }, + }, + required: ['key'], + }; + const errors = validateAgainstSchema(schema, {}); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].message).toContain('required'); + }); + + it('validates type constraints', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + count: { type: 'integer', minimum: 1 }, + }, + }; + const errors = validateAgainstSchema(schema, { count: 0 }); + expect(errors.length).toBeGreaterThan(0); + }); + + it('validates string constraints', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + name: { type: 'string', minLength: 3 }, + }, + }; + const errors = validateAgainstSchema(schema, { name: 'ab' }); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].message).toContain('3'); + }); + + it('validates enum constraints', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + policy: { type: 'string', enum: ['local', 'redis'] }, + }, + }; + const errors = validateAgainstSchema(schema, { policy: 'invalid' }); + expect(errors.length).toBeGreaterThan(0); + }); + + it('validates a limit-count-like schema', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + count: { type: 'integer', minimum: 1 }, + time_window: { type: 'integer', minimum: 1 }, + key: { type: 'string', default: 'remote_addr' }, + rejected_code: { type: 'integer', minimum: 200, maximum: 599, default: 503 }, + policy: { + type: 'string', + enum: ['local', 'redis', 'redis-cluster'], + default: 'local', + }, + }, + }; + + const validData = { + count: 100, + time_window: 60, + key: 'remote_addr', + rejected_code: 503, + policy: 'local', + }; + expect(validateAgainstSchema(schema, validData)).toHaveLength(0); + + const invalidData = { + count: 0, + time_window: -1, + }; + const errors = validateAgainstSchema(schema, invalidData); + expect(errors.length).toBeGreaterThan(0); + }); + + it('strips APISIX-specific fields without error', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + key: { type: 'string' }, + }, + encrypt_fields: ['secret'], + $comment: 'this is a mark', + }; + const errors = validateAgainstSchema(schema, { key: 'test' }); + expect(errors).toHaveLength(0); + }); +}); + +describe('validationErrorsToFieldErrors', () => { + it('maps validation errors to field errors', () => { + const errors = [ + { path: 'name', message: 'is required' }, + { path: 'count', message: 'must be >= 1' }, + ]; + const fieldErrors = validationErrorsToFieldErrors(errors); + expect(fieldErrors.name).toBeDefined(); + expect(fieldErrors.count).toBeDefined(); + }); + + it('uses root for empty path', () => { + const errors = [{ path: '', message: 'schema error' }]; + const fieldErrors = validationErrorsToFieldErrors(errors); + expect(fieldErrors.root).toBeDefined(); + }); +}); diff --git a/src/components/schema-form/fields/AnyOfField.tsx b/src/components/schema-form/fields/AnyOfField.tsx new file mode 100644 index 0000000000..b40234bf54 --- /dev/null +++ b/src/components/schema-form/fields/AnyOfField.tsx @@ -0,0 +1,91 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Box, Fieldset, SegmentedControl, Text } from '@mantine/core'; +import { useState } from 'react'; + +import type { FieldProps } from '../types'; +import { getFieldDescription, getFieldLabel, getOneOfLabel } from '../utils'; +import { SchemaField } from './SchemaField'; + +export const AnyOfField: React.FC = ({ + schema, + name, + required: _required, + encryptFields, + disabled, +}) => { + const anyOfSchemas = schema.anyOf ?? []; + if (anyOfSchemas.length === 0) return null; + + const [selectedIndex, setSelectedIndex] = useState(0); + const label = getFieldLabel(name, schema); + const description = getFieldDescription(schema); + + const options = anyOfSchemas.map((s, i) => ({ + value: String(i), + label: getOneOfLabel(s, i), + })); + + const selectedSchema = anyOfSchemas[selectedIndex]; + if (!selectedSchema) return null; + + return ( +
+ {description && ( + + {description} + + )} + {anyOfSchemas.length > 1 && ( + + + Select format + + setSelectedIndex(Number(val))} + data={options} + disabled={disabled} + fullWidth + size="xs" + /> + + )} + {selectedSchema.properties && + Object.entries(selectedSchema.properties).map(([key, propSchema]) => ( + + + + ))} + {selectedSchema.type && !selectedSchema.properties && ( + + )} +
+ ); +}; diff --git a/src/components/schema-form/fields/ArrayField.tsx b/src/components/schema-form/fields/ArrayField.tsx new file mode 100644 index 0000000000..10173b354b --- /dev/null +++ b/src/components/schema-form/fields/ArrayField.tsx @@ -0,0 +1,214 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ActionIcon, + Box, + Button, + Fieldset, + Group, + TagsInput, + Text, +} from '@mantine/core'; +import { useController, useFieldArray, useFormContext } from 'react-hook-form'; + +import type { FieldProps, JSONSchema } from '../types'; +import { getFieldDescription, getFieldLabel, getSchemaType } from '../utils'; +import { SchemaField } from './SchemaField'; + +export const ArrayField: React.FC = ({ + schema, + name, + required, + encryptFields, + disabled, +}) => { + const label = getFieldLabel(name, schema); + const description = getFieldDescription(schema); + + const itemSchema = (schema.items && !Array.isArray(schema.items)) + ? schema.items + : undefined; + + const itemType = itemSchema ? getSchemaType(itemSchema) : 'string'; + + // For simple string/number arrays, use TagsInput + if (itemType === 'string' && !itemSchema?.enum && !itemSchema?.properties) { + return ( + + ); + } + + // For object arrays, render repeatable fieldsets + if (itemSchema && (itemType === 'object' || itemSchema.properties)) { + return ( + + ); + } + + // Fallback: simple tags input + return ( + + ); +}; + +const SimpleStringArray: React.FC<{ + schema: JSONSchema; + name: string; + label: string; + description?: string; + required?: boolean; + disabled?: boolean; +}> = ({ schema, name, label, description, required, disabled }) => { + const { control } = useFormContext(); + const { + field: { value, onChange, onBlur, ref }, + } = useController({ + name, + control, + defaultValue: schema.default ?? [], + }); + + return ( + onChange(vals)} + acceptValueOnBlur + placeholder="Type and press Enter" + /> + ); +}; + +const ObjectArrayField: React.FC<{ + schema: JSONSchema; + itemSchema: JSONSchema; + name: string; + label: string; + description?: string; + required?: boolean; + encryptFields?: string[]; + disabled?: boolean; +}> = ({ + schema, + itemSchema, + name, + label, + description, + required, + encryptFields, + disabled, +}) => { + const { control } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + control, + name, + }); + + const maxItems = schema.maxItems; + const canAdd = !maxItems || fields.length < maxItems; + + return ( +
+ {description && ( + + {description} + + )} + {required && fields.length === 0 && ( + + At least one item is required + + )} + {fields.map((field, index) => ( + + + + Item {index + 1} + + remove(index)} + disabled={disabled} + aria-label={`Remove item ${index + 1}`} + > + ✕ + + + {itemSchema.properties && + Object.entries(itemSchema.properties).map(([key, propSchema]) => ( + + + + ))} + + ))} + +
+ ); +}; diff --git a/src/components/schema-form/fields/BooleanField.tsx b/src/components/schema-form/fields/BooleanField.tsx new file mode 100644 index 0000000000..f53c51aa09 --- /dev/null +++ b/src/components/schema-form/fields/BooleanField.tsx @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Switch } from '@mantine/core'; +import { useController, useFormContext } from 'react-hook-form'; + +import type { FieldProps } from '../types'; +import { getFieldDescription, getFieldLabel } from '../utils'; + +export const BooleanField: React.FC = ({ + schema, + name, + disabled, +}) => { + const { control } = useFormContext(); + const { + field: { value, onChange, onBlur, ref }, + fieldState: { error }, + } = useController({ + name, + control, + defaultValue: schema.default ?? false, + }); + + const label = getFieldLabel(name, schema); + const description = getFieldDescription(schema); + + return ( + onChange(e.currentTarget.checked)} + labelPosition="left" + /> + ); +}; diff --git a/src/components/schema-form/fields/EnumField.tsx b/src/components/schema-form/fields/EnumField.tsx new file mode 100644 index 0000000000..a0b9b1955e --- /dev/null +++ b/src/components/schema-form/fields/EnumField.tsx @@ -0,0 +1,77 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Select } from '@mantine/core'; +import { useController, useFormContext } from 'react-hook-form'; + +import type { FieldProps } from '../types'; +import { getFieldDescription, getFieldLabel } from '../utils'; + +export const EnumField: React.FC = ({ + schema, + name, + required, + disabled, +}) => { + const { control } = useFormContext(); + const { + field: { value, onChange, onBlur, ref }, + fieldState: { error }, + } = useController({ + name, + control, + defaultValue: schema.default ?? undefined, + rules: { + required: required ? 'This field is required' : false, + }, + }); + + const label = getFieldLabel(name, schema); + const description = getFieldDescription(schema); + + const options = (schema.enum ?? []).map((val) => ({ + value: String(val), + label: String(val), + })); + + return ( +