Custom Form Control
Requirements
Supported Environments
- Private deployment
- SAAS
ONES System Version
v6.90.0+
ONES CLI Version
v1.70.1+
npm install -g @ones/cli --registry=https://npm.partner.ones.cn/registry/
ones --version
1.70.1
Capability Overview
This document describes how to add a custom form control to the work item detail form, create form, and transition form via feature extensions.
Extension Configuration
Configuration
Add the following configuration to config/plugin.yaml.
oauth:
type:
- user
scope:
- read:project:issueField
extension:
- formControlExtension:
provider: smsprov2
slots:
- name: ones:form:control:content
entryUrl: modules/ones-form-control-content-nLtK/index.html
- name: ones:form:control:settings
entryUrl: modules/ones-form-control-settings-71OJ/index.html
config:
name: Custom control name
fieldUUIDs:
- 9ax9ExKt
- M3Fgzu3F
- EGx9jsaH
- LS5aqwaW
supportForms:
- detail
- create
- transition
controlTips: Control description
Note: The oauth-related configuration is mainly related to the Open API. Configure the plugin as needed. See the documentation: Open API
Slot Description
| Plugin Name | Description |
|---|---|
| ones:form:control:content | The slot where the custom form control renders its content in the form. |
| ones:form:control:settings | The slot to implement custom settings in the form editor. If custom settings are not needed, omit this. |
config Field Description
| Field | Type | Description |
|---|---|---|
| name | string | Custom form control name |
| fieldUUIDs | string[] | List of fields included in the custom form control |
| supportForms | "detail" | "create" | "transition" | Forms supported by the custom control. Currently only three (detail, create, transition). create requires detail. |
| controlTips | string | Control description shown in the form editor. Optional. |
Example

Frontend Implementation
Custom Form Control Content Slot
- Create the entry file for rendering the slot content:
web/src/modules/ones-form-control-content-nLtK/index.tsx.
import React from 'react'
import ReactDOM from 'react-dom'
import { ConfigProvider } from '@ones-design/core'
import { lifecycle, OPProvider } from '@ones-op/bridge'
import './index.css'
import { ControlContentExtension } from './control_content_extension'
ReactDOM.render(
<ConfigProvider>
<OPProvider>
<ControlContentExtension/>
</OPProvider>
</ConfigProvider>,
document.getElementById('ones-mf-root'),
)
lifecycle.onDestroy(() => {
ReactDOM.unmountComponentAtNode(document.getElementById('ones-mf-root') as HTMLElement)
})
- Implement the
ControlContentExtensioncomponent.
import React, { useEffect } from 'react'
import type { FC } from 'react'
import { useExtensionContext, useExtensionConfig } from '@ones-op/sdk'
export const ControlContentExtension: FC = () => {
// Inside the plugin, call useExtensionContext to obtain the context passed by the product.
// Call useExtensionConfig to obtain the plugin configuration.
const extensionContext = useExtensionContext()
const extensionConfig = useExtensionConfig()
const {
fields,
fieldsConfig,
ctx,
values,
onSubmit,
onPermissionDenied,
type,
inEditor,
onChange,
settings,
} = extensionContext
return <div>your business logic</div>
}
extensionContextParameter Description
| Field | Type | Description | Detail Form | Create Form | Transition Form |
|---|---|---|---|---|---|
| name | string | Custom form control name | ✅ | ✅ | ✅ |
| inEditor | boolean | Whether it is in the form editor | ✅ | ✅ | ✅ |
| type | "detail" | "create" | "transition" | Current form type | ✅ | ✅ | ✅ |
| fields | IField[] | Basic field information | ✅ | ✅ | ✅ |
| fieldsConfig | Record<string, FieldConfig> | Field configuration in the form, including: visible, required, placeholder, hasPermission | ✅ | ✅ | ✅ |
| values | Record<string, any> | Field values in the form | ✅ | ✅ | ✅ |
| settings | Record<string, any> | Configuration of the custom control on the form | ✅ | ✅ | ✅ |
| ctx | ControlContentExtensionContext | Context information | ✅ | ✅ | ✅ |
| onPermissionDenied | (fieldUUID: string) => void | Shows a toast for lack of permission by fieldUUID; handled by product | ✅ | ||
| onSubmit | (values: Record<string, { value: unknown; submitType?: SubmitTypeEnum }> ) => Promise<void> | Callback to submit data | ✅ | ||
| onChange | (value: Record<string, any>) => void; | Callback when data changes in create/transition forms | ✅ | ✅ |
ControlContentExtensionContext Type
export interface ControlContentExtensionContext {
taskUUID?: string // Work item UUID; provided only in detail and transition forms
projectUUID?: string // Project UUID
issueTypeUUID?: string // Work item type UUID
taskDetailViewMode?: TaskDetailViewMode // Work item detail layout; only provided in detail form
taskStatus?: TaskStatus // Work item status; detail form only
transitionUUID?: string // Transition UUID; only available in transition form
formValues?: Record<string, any> // Form values; only in create and transition forms
}
export enum TaskDetailViewMode {
Wide = 'wide', // Wide detail layout
Narrow = 'narrow', // Narrow detail layout
}
export interface TaskStatus {
uuid: string
category: string
name: string
}
-
Flow Description:
Detail form: Call
onSubmitwith the data edited inside the custom control. After listening toonSubmit, the ONES will call the update API with the received parameters to update work item fields.Create and transition forms: Call
onChangewith the data edited inside the custom control. The ONES listens toonChange, saves the received values into the form, and submits all data together when the confirm button is clicked.
Custom Form Control Settings Slot
- Create the entry file for the custom settings slot:
web/src/modules/ones-form-control-settings-71OJ/index.tsx.
import React from 'react'
import ReactDOM from 'react-dom'
import { ConfigProvider } from '@ones-design/core'
import { lifecycle, OPProvider } from '@ones-op/bridge'
import { ControlSettingsExtension } from './control_settings_extension'
import './index.css'
ReactDOM.render(
<ConfigProvider>
<OPProvider>
<ControlSettingsExtension />
</OPProvider>
</ConfigProvider>,
document.getElementById('ones-mf-root'),
)
lifecycle.onDestroy(() => {
ReactDOM.unmountComponentAtNode(document.getElementById('ones-mf-root') as HTMLElement)
})
- Implement the
ControlSettingsExtensioncomponent.
import React, { useEffect } from 'react'
import type { FC } from 'react'
import { useExtensionContext, useExtensionConfig } from '@ones-op/sdk'
export const ControlSettingsExtension: FC = () => {
// Inside the plugin, call useExtensionContext to obtain the context passed by the ONES.
// Call useExtensionConfig to obtain the plugin configuration.
const extensionContext = useExtensionContext()
const extensionConfig = useExtensionConfig()
return <div>your business logic</div>
}
extensionContextParameter Description
| Name | Type | Description |
|---|---|---|
| name | string | Plugin name |
| type | 'detail' | 'create' | 'transition' | Form type |
| fields | IField[] | Basic field information |
| value | Record<string, any> | Previously saved settings |
| ctx | ControlSettingsExtensionContext | Context information |
| onChange | (value: Record<string, any>) => Promise<void> | Callback on settings change |
ControlSettingsExtensionContext Type
export interface ControlSettingsExtensionContext {
projectUUID?: string // Project UUID
issueTypeUUID?: string // Work item type UUID
transitionUUID?: string // Transition UUID; only available in transition forms
}
- Flow Description:
The plugin passes the edited data back to the ONES via onChange. The ONES listens to onChange and saves the received parameters into the form configuration. During rendering, the configuration is read and passed to the plugin via the settings field.