senior
Как создать Custom Field Type?
Custom Field Type позволяет создать собственный тип поля задачи с кастомной логикой ввода, отображения, валидации и поиска.
Объявление и реализация
atlassian-plugin.xml и PriorityScoreField
<customfield-type key="priority-score-field"
class="com.example.plugin.field.PriorityScoreField">
<name>Priority Score</name>
<description>Числовой приоритет с расчётом на основе severity и impact</description>
<resource name="view" type="velocity"
location="templates/fields/priority-score-view.vm"/>
<resource name="edit" type="velocity"
location="templates/fields/priority-score-edit.vm"/>
<resource name="xml" type="velocity"
location="templates/fields/priority-score-xml.vm"/>
</customfield-type>
@Named
public class PriorityScoreField extends AbstractSingleFieldType<Double> {
private static final Logger log = LoggerFactory.getLogger(PriorityScoreField.class);
@Inject
public PriorityScoreField(
@ComponentImport CustomFieldValuePersister persister,
@ComponentImport GenericConfigManager configManager) {
super(persister, configManager);
}
@Override
protected PersistenceFieldType getDatabaseType() {
return PersistenceFieldType.TYPE_DECIMAL;
}
@Override
protected Object getDbValueFromObject(Double value) {
return value;
}
@Override
protected Double getObjectValue(String dbValue) {
if (dbValue == null) return null;
try {
return Double.parseDouble(dbValue);
} catch (NumberFormatException e) {
log.warn("Невозможно распарсить значение: {}", dbValue);
return null;
}
}
@Override
public String getStringFromSingularObject(Double value) {
return value != null ? value.toString() : "";
}
@Override
public Double getSingularObjectFromString(String s)
throws FieldValidationException {
if (s == null || s.trim().isEmpty()) return null;
try {
double value = Double.parseDouble(s);
if (value < 0 || value > 100) {
throw new FieldValidationException(
"Priority Score должен быть от 0 до 100");
}
return value;
} catch (NumberFormatException e) {
throw new FieldValidationException("Некорректное числовое значение: " + s);
}
}
@Override
public Map<String, Object> getVelocityParameters(Issue issue,
CustomField field, FieldLayoutItem fieldLayoutItem) {
Map<String, Object> params = super.getVelocityParameters(
issue, field, fieldLayoutItem);
Double value = (Double) issue.getCustomFieldValue(field);
if (value != null) {
String cssClass;
if (value >= 80) cssClass = "aui-lozenge-error";
else if (value >= 50) cssClass = "aui-lozenge-current";
else cssClass = "aui-lozenge-success";
params.put("cssClass", cssClass);
params.put("formattedValue", String.format("%.1f", value));
}
return params;
}
}
Velocity-шаблоны
Пример
<!-- templates/fields/priority-score-edit.vm -->
<input type="text" id="$customField.id" name="$customField.id"
value="$!{value}" class="text short-field" placeholder="0-100"/>
<div class="description">Введите числовое значение от 0 до 100</div>
Пример
<!-- templates/fields/priority-score-view.vm -->
#if($formattedValue)
<span class="aui-lozenge $cssClass">$formattedValue</span>
#else
<span class="aui-lozenge">Не задано</span>
#end
Частые ошибки
- Не реализовать Searcher — поле нельзя использовать в JQL и фильтрах
- Не обрабатывать null/пустые значения в
getSingularObjectFromString()— NPE при создании задачи - Тяжёлая логика в
getVelocityParameters()— вызывается при каждом отображении задачи - Изменение
getDatabaseType()после деплоя — несовместимость с существующими данными
Как используется в 2026
- Custom Field Type остаётся мощным инструментом для DC
- В Cloud кастомные поля создаются через Forge UI Kit или Connect (iframe)
- Тренд на использование JSON-полей (StringLength.UNLIMITED) для сложных структур данных
На собеседовании: покажите понимание жизненного цикла кастомного поля: edit (ввод) -> validate -> persist -> view (отображение). Для простых типов наследуйтесь от AbstractSingleFieldType, для множественных значений — от AbstractMultiCFType. Searcher обязателен для доступа поля в JQL.