Gymterview
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.