Custom Ticket Fields Developer Guide - edmozley/freeitsm GitHub Wiki
How custom ticket fields (discussion #179) work underneath. For what they do, see Custom ticket fields.
| File | What it does |
|---|---|
includes/services/ticket_fields.php |
TicketFieldsService - which fields a ticket has, validation, writing with history, the closing check, and designing fields |
includes/typed_fields.php |
TypedFields - the shared engine: types, coercion, required rule, value storage. Shared with the CMDB and custom asset fields |
includes/services/tickets.php |
createTicket() / updateTicket() take custom_fields
|
includes/services/mandatory_fields.php |
missing() adds custom fields required at closing |
includes/self_service_ticket_types.php |
The portal's types and company, shared by its form and its create |
assets/js/ticket-fields.js + assets/css/ticket-fields.css
|
One renderer for the New ticket form, the ticket panel and the portal |
api/tickets/get_ticket_custom_fields.php / save_ticket_custom_fields.php
|
The inbox's read and per-field save |
api/tickets/get_ticket_fields_settings.php, save_ticket_field.php, delete_ticket_field.php, reorder_ticket_fields.php
|
The settings tab |
api/self-service/get_ticket_fields.php |
The portal form: types (when they matter) and asked fields |
api/self-service/create_ticket.php |
The portal create: type and answers re-checked |
api/v1/resources/tickets.php |
custom_fields on GET / POST / PATCH |
tests/ticket-custom-fields.php |
The test (section 9) |
| Table | Holds |
|---|---|
ticket_fields |
the definitions: field_key (immutable), label, field_type, config JSON, help_text, all_types, required_on_create, required_on_close, ask_requester, display_order, tenant_id, is_deleted
|
ticket_field_options |
dropdown choices; the answer stores the option's text |
ticket_field_types |
which types a field is on when all_types = 0
|
ticket_field_values |
the answers, one typed column each - the same shape as asset_field_values
|
-
Soft delete only.
ticket_field_values.field_idis aRESTRICTforeign key indatabase/freeitsm.sql, so a field with answers can only be retired. -
seqis always 0, reserved so a multi-value field needs no index change later (Database Verification restores columns, not a changed unique index). -
uq_ticket_fields_tenant_keydoes not dedupe shared keys (MySQL treats NULLs as distinct), sofreeKey()checks in code - the trapasset_fieldsdocuments. -
No
tenant_idon values - inherited from the ticket. -
ref(link fields) is not offered on tickets: it needs a picker.TicketFieldsService::TYPESis the list.
Resolved in one place, TicketFieldsService::fieldsFor($tenantId, $typeId, $audience):
not retired and (shared or the ticket's company's own) and (on every type or linked to the ticket's type) and, for
'requester',ask_requester = 1.
Everything else asks it: the New ticket form, the panel, the portal, the API, the closing rule. Two copies of this rule would be the bug.
๐ Type changes hide, never delete. TypedFields::readValues() only returns values for the definitions it is handed, so an answer to a field the new type lacks is simply not read. Nothing removes it.
toDef() sets the engine's required to required_on_create, so TypedFields::checkRequired() enforces it with the CMDB's asymmetry: missing altogether is refused on create; on update only fields being written are checked - so a required field cannot be cleared, but editing another field on an older ticket never fails because of it.
Logging - only the channels that can ask:
| Channel |
custom_fields passed? |
Enforced? |
|---|---|---|
New ticket form (api/tickets/create_ticket.php) |
yes, always | โ |
REST API POST /tickets
|
yes - defaulted to []
|
โ |
Portal (api/self-service/create_ticket.php) |
yes, requester fields only | โ for asked fields |
Morning checks' raise-a-ticket (same endpoint, no custom_fields) |
no | โ |
| Email, chat, workflows, project and domain alerts | no | โ |
๐ createTicket() enforces only when the custom_fields key is present. Present means "this channel can ask". That is the switch - an alert raised by the system must never be refused because nobody filled in a form it never showed.
Closing - MandatoryFieldsService::missing($conn, $tenant, $row, $pendingCustom) appends TicketFieldsService::missingOnClose() as cf:<key> => label, even when no built-in field is ticked, so the tab's mode (warn / notify / block) and its note and email cover custom fields too. $pendingCustom is the answers being written by the same updateTicket(), so filling a field in the save that closes the ticket counts. The inbox's pre-close check (get_mandatory_fields_check.php) gets the label through t()'s fallback to English - the name an admin typed.
-
Validate first, write later.
validate()runs unknown-key, required, email / URL format, length and everyTypedFields::coerce()rule before anything is written. IncreateTicket()and the portal create it runs before theINSERT, so a bad answer never leaves a half-made ticket. -
updateTicket()resolves the definitions againstrowAfterUpdates()- a type changed in the same request decides which fields apply - and writes after theUPDATE. A save with only custom fields still touchesupdated_datetime. -
History is always server-side.
write()adds oneticket_auditrow per changed field,field_name= the label (cut to 100), values as a person reads them (displayValue(), cut to 500), whatever$writeAuditsays - the UI never logs these client-side. A portal answer is audited with a NULL analyst.
assets/js/ticket-fields.js exposes TicketFields.render(fields, opts), collect(container) and firstMissing(container, fields).
-
variant: 'toolbar'- the panel. Rendered into#ticketCustomFields(display: contents) inside.ticket-toolbar, so each field is a real grid item beside the built-in ones and the phone layout's restyling of the toolbar applies unchanged. -
variant: 'form'- the New ticket forms, with the form's own classes (inputClass/selectClass/labelClass) so a custom field is indistinguishable from the built-in ones. -
collect()sends an empty control as''- clear - never omits it. - Yes / no has three states, Not set included, exactly as the asset renderer.
- The panel reloads its fields after a type change (
assignTicketType()); the New ticket form after a type or company change, keeping answers already typed.
selfServiceTicketTypeMatters() - the picker appears only when an asked field is on particular types; otherwise choosing a type would change nothing. Types offered and types accepted come from one function, selfServiceTicketTypes(), so a hand-posted type the company does not offer is dropped, and a hand-posted key for an analyst-only field is a refusal (fieldsFor(..., 'requester') does not contain it). A chosen type also numbers the ticket per type, as other channels do.
designScope() copies ticket types: working in the default company (or on a one-company install) manages shared fields; working in a client company manages that company's own, and sees shared ones read-only. loadEditable() enforces it on every save, retire and reorder, and a field may only be linked to types that context can see (assertTypeUsable()). The capability is Cap::TICKETS_CUSTOM_FIELDS.
php tests/ticket-custom-fields.php - 29 checks on the real tables with throwaway ZZTF types, fields and one ticket inserted directly (no workflow, email or notification fires), removed at the end:
- which fields a type has, and what the requester is asked;
- required-when-logging refusals, including
createTicket()refusing before any row is written; - dropdown, email and unknown-key refusals;
- answers stored, shown, audited; a required one cannot be cleared;
- a type change hides an answer, the row stays, changing back restores it;
- missing at closing, and not missing when the same save fills it in;
- the kind lock, a second field with the same name getting
_2, retiring keeping answers; - the portal's type picker rule.
Checked in a browser as well: the settings tab and dialog, the New ticket form, the panel, and the portal on desktop and phone - and the portal create refusing a missing answer, an analyst-only key and a bad option without creating a ticket.
- Workflow conditions (
engine.phpfield lists), email merge codes (buildTicketMergeData()), report packs and dashboard widgets, search - each is a hard-coded list that would have to learncf:<key>. - Split and merge do not copy answers to the new ticket.
- The portal ticket page does not show the requester their answers.
- Translations: field names and options are data, like ticket type names, and are shown as typed.
- Custom asset fields - Developer Guide - the same engine, with sets
- Mandatory fields - Developer Guide