Skip to main content

Spark Workbench Custom Field Type Plugin Guide

This guide is for developers extending Spark Workbench task field types. A third-party plugin should be able to ship field type definitions, system field definitions, field configuration logic, TQL operators, and matching frontend Vue components in the same way as the Spark default plugin.

The default field plugin is located at:

spark-plugins/spark-workbench-default-plugin/

Spark built-in field types such as Text, Rich Text, Markdown, User, and Attachment, and system fields such as Title, Assignee, and Description, are registered by this plugin. The platform host only consumes extension points; concrete field types are no longer hard-coded in the task designer.

1. Field Plugin Model​

A field type plugin usually has two parts:

  • Backend extension point: returns field types, system fields, configuration forms, supported TQL operators, and filter protocol through a Lattice Ability.
  • Frontend component: renders create / edit controls with Vue, and may also provide read-only and task-detail components.

Recommended structure:

my-task-field-plugin/
pom.xml
src/main/java/
.../MyTaskFieldPlugin.java
.../MyFieldRegistryExt.java
.../PriorityFieldTypeProvider.java
src/main/frontend/
package.json
vite.config.js
src/
index.js
PriorityField.vue
PriorityDetailField.vue

The plugin jar should contain both Java extension points and frontend static assets. Spark loads them at runtime.

2. Business Identity​

The plugin must declare its own Lattice business identity:

@Business(code = "my.task.field.plugin", name = "My Task Field Plugin")
public class MyTaskFieldPlugin extends BusinessTemplate {
}

When registering fields, Spark iterates over all registered Lattice business identities and calls the field registry Ability for each identity. Third-party plugins should not attach @Realization to a platform built-in code; use the plugin's own business identity.

3. Registering Field Types​

The field registry extension point is SparkEraTaskFieldRegistryExt. In most cases, extend the blank implementation:

@Realization(codes = "my.task.field.plugin")
public class MyFieldRegistryExt extends BlankSparkEraTaskFieldRegistryExt {

private static final String FRONTEND_RESOURCE =
"/spark-gadgets/my-task-field-plugin/index.umd.js";

@Override
public List<SparkEraTaskCustomFieldTypeProvider> listCustomFieldTypeProviders(
SparkEraTaskFieldRegistryContext context) {
return List.of(new PriorityFieldTypeProvider(FRONTEND_RESOURCE));
}
}

Example field type provider:

public class PriorityFieldTypeProvider implements SparkEraTaskCustomFieldTypeProvider {

private final String frontendResource;

public PriorityFieldTypeProvider(String frontendResource) {
this.frontendResource = frontendResource;
}

@Override
public String getCode() {
return "priority_score";
}

@Override
public String getTitle() {
return "Priority Score";
}

@Override
public SparkEraTaskFieldCategory getCategory() {
return SparkEraTaskFieldCategory.BASIC;
}

@Override
public String getDescription() {
return "A numeric priority score with visual emphasis.";
}

@Override
public String getComponentType() {
return "priorityScore";
}

@Override
public String getTaskFieldType() {
return "number";
}

@Override
public String getPreviewIcon() {
return "gauge";
}

@Override
public boolean isSortable() {
return true;
}

@Override
public List<String> supportOperators() {
return List.of("eq", "neq", "gt", "ge", "lt", "le");
}

@Override
public String getFrontendResource() {
return frontendResource;
}

@Override
public String getEditComponent() {
return "my$field-priority-score";
}

@Override
public String getDetailComponent() {
return "my$task-detail-priority-score-field";
}
}

Key fields:

  • code: globally unique field type code.
  • componentType: runtime control semantics.
  • taskFieldType: underlying value type, used by query, indexing, and display.
  • previewIcon: field palette icon in the task designer.
  • supportOperators: TQL operators supported by this field. Do not return the full operator set by default.
  • frontendResource: plugin frontend UMD entry.
  • editComponent: Vue component key for create / edit forms.
  • readonlyComponent: Vue component key for ordinary read-only forms; optional.
  • detailComponent: task detail render-protocol component key.

4. Registering System Fields​

System fields use SparkEraTaskSystemFieldProvider:

@Override
public List<SparkEraTaskSystemFieldProvider> listSystemFieldProviders(
SparkEraTaskFieldRegistryContext context) {
return List.of(new EscalationLevelSystemFieldProvider(FRONTEND_RESOURCE));
}

A system field provider should return a stable code, display label, field type, listability, searchability, sortability, and supported operators. System fields can also declare frontendResource and component keys.

5. Frontend Component Registration​

The plugin frontend entry registers components through window.__SPARK_TASK_FIELDS__:

import PriorityField from "./PriorityField.vue";
import PriorityDetailField from "./PriorityDetailField.vue";

const api = typeof window === "undefined" ? null : window.__SPARK_TASK_FIELDS__;

if (api?.registerFieldComponent) {
api.registerFieldComponent("my$field-priority-score", PriorityField);
}

if (api?.registerDetailFieldComponent) {
api.registerDetailFieldComponent("my$task-detail-priority-score-field", PriorityDetailField);
}

Create / edit components receive these main props:

defineProps({
modelValue: [String, Number, Boolean, Array, Object],
field: Object,
definition: Object,
placeholder: String,
disabled: Boolean,
context: Object
});

Use emit("update:modelValue", value) to write the field value back. context contains host utilities such as t, apiFetch, openUserSelect, and chooseAttachmentFiles. Third-party components should reuse host capabilities instead of reimplementing common platform interactions.

Task-detail components use the Spark render protocol and receive componentModel, renderContext, and t:

<template>
<div class="priority-detail">
<span>{{ field.label }}</span>
<strong>{{ field.value }}</strong>
</div>
</template>

<script setup>
import { computed } from "vue";

const props = defineProps({
componentModel: Object
});

const field = computed(() => props.componentModel?.props?.field || {});
</script>

6. Vite Build​

Field plugin frontends use Vite library mode just like Gadget plugins, with Vue marked as external:

import { resolve } from "node:path";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
plugins: [vue()],
build: {
outDir: resolve(__dirname, "../../../target/classes/META-INF/resources/spark-gadgets/my-task-field-plugin"),
lib: {
entry: resolve(__dirname, "src/index.js"),
name: "MyTaskFieldPlugin",
formats: ["umd"],
fileName: () => "index.umd.js",
cssFileName: "index"
},
rollupOptions: {
external: ["vue"],
output: {
globals: {
vue: "__SPARK_VUE__"
}
}
}
}
});

After build, the runtime resource URL is:

/spark-gadgets/my-task-field-plugin/index.umd.js

7. Development Checklist​

  • Field type codes and system field codes must be stable.
  • @Realization(codes = "...") must use the plugin's own business identity.
  • Query operators must match the field value type; do not return every operator.
  • Sortability should be declared by the field type. Rich text, attachments, and long text are usually not good sort fields.
  • Frontend components must not bundle a separate Vue runtime.
  • Component styles should use Spark theme variables instead of hard-coded colors.
  • Field labels, descriptions, and configuration item text should be returned through the backend i18n mechanism.