Form Fields Design - edmozley/freeitsm GitHub Wiki
Simpler ticket and task forms - design (#218)
Status: built - Parts A, B and C are in 4.9.0. Part D stays deferred. @Dr4inder reviewed this page on issue #218 on 2026-10-29 and approved Parts A to C, with changes to how Part A treats Department, which are now written in below. Build order: B, then C, then A. Part D is deferred to a separate follow-up and is not part of this request. The reviewer's answers, and what each changed, are in section 8.
The request, from issue #218 (raised by @Dr4inder):
"A small internal service desk may not need every field offered by the ticket and task forms. Even when the underlying modules remain useful, unnecessary selectors make routine work harder to scan."
In plain words
The issue asks for three things, in the post and two follow-up comments. I added a fourth, which is now deferred.
| Part | What it does | Size | Status |
|---|---|---|---|
| B. Calmer task Work view (comment 1) | Checklists, Forms and Follow-ups with nothing in them shrink to a heading such as Checklists (0), and open with one click | Small | Built (4.9.0); Procedures added by PR #247 |
| C. Portal questions in the main form (comment 2) | On the portal's New ticket page, the custom field questions move out of the side panel and into the main form, between the subject and the description | Small | Built (4.9.0) |
| A. Built-in fields (the post) | Hide the ticket Department field, or fill it in from a default. Ask for the ticket Company only when the answer is not obvious. One switch hides the task's Contractor and Contact there pair | Large | Built (4.9.0) |
| D. Custom fields for chosen departments (addition) | A custom field can be limited to some departments, the same way it can already be limited to some ticket types | Medium | Deferred - a separate follow-up |
Each part is useful on its own and ships on its own.
Three rules run through every part:
- Nothing changes on upgrade. Every new switch starts where today's behaviour is. An administrator opts in.
- Hiding is presentation, never deletion. A hidden field keeps its value. Saving an unrelated change never clears it. Switching it back on shows it again, exactly as it was.
- Hiding is not access control, and never weakens another rule. Permissions, company boundaries and the REST API stay as they are. A hidden field does not switch off a rule that was set up separately, such as Mandatory fields - if the two conflict, the settings screen refuses the combination and says how to fix it.
1. Is this a natural extension, or a second system?
The risk with "configurable form fields" is a general form builder bolted beside the existing settings, which then disagrees with them. This design avoids that by only adding switches where FreeITSM already has the matching machinery:
- Per-company settings already exist.
tenantSetting()(inincludes/tenant_settings.php) answers "what does this company say, or else what does the install say". The category, category-at-close and resolution-code switches on Tickets -> Settings -> Categories already use it, with a per-company table of Follow default / On / Off. Part A copies that screen and that storage. - Saves are already patch-style. Saving a ticket or a task only writes the fields the request sends (
TicketsService::updateTicket(),TasksService::saveTask()), so a hidden control cannot null its value by being absent. Rule 2 holds by construction, and the tests will prove it. - One closure rule.
MandatoryFieldsServicealready decides what a ticket needs before it closes. Part A does not teach it to skip Department; it stops the conflicting configuration from being saved in the first place (2.2).
So this is three small extensions of existing parts, not a new system.
2. Part A - built-in fields
2.1 Where the settings live
- Tickets -> Settings -> Fields - the existing Custom fields tab, renamed, with a new Built-in fields panel above the custom field list. One place for "what this form asks". As built (4.9.0): the tab kept its name, Custom fields, and the panel is Department field.
- Tasks -> Settings -> Involved - the Contractor fields on tasks switch. (Changed while building: Tasks settings has no General tab. Involved is the tab about who works on a task, which is what a contractor is.)
Both use the install-wide default + per-company override pattern from the Categories tab: a default row of switches, then a table with one row per company (Follow default / On / Off). On a single-company install the table is not shown, so it is just a switch.
2.2 Department
Settings, per company:
| Setting | Choices | Default |
|---|---|---|
| Show the Department field | On / Off | On |
| Department for new tickets | None / any active department | None |
Where "Off" hides it. Every place the field appears, so no gap is left:
- the New ticket form;
- the ticket panel, the ticket workspace and the collapsed header's Dept line;
- the right-click Department menu, the bulk Department button and drag-and-drop onto a department folder;
- the Department column in the ticket table's column picker;
- the phone view, which shares the same code.
A ticket that already has a department keeps it. The value stays in the database, in reports, in the email merge field [department_name], in workflow conditions and in history.
What a department still does while hidden. This is the important part, and the reason Department needs more care than the other fields:
| Department drives | With the field hidden |
|---|---|
| Which folder a ticket sits in | Unchanged. Departments do not decide who may open a ticket - company access does that, and the ticket list has no department filter. A team's departments only decide which department folders (and dropdown choices) its analysts see (get_my_departments.php). A ticket with no department sits in the no department folder and the general views, not in a department's folder - so an analyst who only watches their department folder could miss it, but can always find and open it. (Corrected 2026-11-03: this row first said a ticket with no department is "visible to every team", which wrongly suggested departments restrict who can see a ticket.) |
| SLA notification rules per department | Unchanged. A ticket with no department matches only the default rules, as today |
| Notify department teams | A ticket with no department notifies no department teams, as today |
| Confidential departments | Unchanged. A workflow or the default department can still put a ticket in one |
Workflows (set_ticket_department, conditions) |
Unchanged. A workflow may still set or test the department |
So the settings screen says plainly, when Department is hidden with no default: "Hiding Department only removes the field from the screen - nobody loses access to any ticket. New tickets will have no department, so they will not appear in any department's folder, and department SLA and notification rules will not apply to them. To keep those working, choose a department for new tickets, or let a workflow set it." Hidden with no default is allowed - it is the right setting when departments are genuinely unused.
The default department
Applied once, at creation, when nothing else has given the ticket a department, on every channel that can create one: the New ticket form, the portal, email, chat, the REST API and catalogue approvals.
- An explicit department wins. One set on the New ticket form, by a REST API caller or by a catalogue item is kept. (Mailboxes cannot set a department today, so there is no mailbox mapping to preserve.)
- An invalid explicit department is refused, not replaced. A REST call naming a department that does not exist or is inactive gets an error, exactly as today. The default is for "none given", never a quiet correction of a wrong answer.
- It is resolved before anything that depends on the department - create-time validation, and anything that decides which questions a ticket has - so they all see the department the ticket will be created with. (Built: the portal, email, messaging and web chat missed this for the ticket type rules until #248.)
- Workflows that run after creation may still change it, as they can today.
- Never on edit, never on existing tickets. A ticket that already exists without a department keeps having none until somebody gives it one.
Checked again at creation. If the default is no longer an active department when a ticket is created (which the guard below should prevent, but a database edit could cause), the ticket is created without one - and not silently: the ticket's history says "Default department 'HR' is inactive, so it was not applied", and the Built-in fields panel shows a red warning for that company until the default is fixed. A stale setting must not quietly drop new tickets out of every department's folder and rules.
Guards when the setting, or a department, changes
- Hidden while required at closure: refused. If Mandatory fields requires Department at closure for a company, Department cannot be hidden for that company: "Department is required at closure for Acme. Untick it on Mandatory fields, or keep Department shown." The same check runs the other way: ticking Department on Mandatory fields while it is hidden for some companies is refused, naming them. The closure rule is never quietly switched off by a presentation setting, and no ticket - including an old one with no department - can be trapped behind a field nobody can fill. (A hidden Department that must be filled in makes no sense anyway: hiding it says departments are not used.)
- The default must be an active department. An inactive one cannot be chosen.
- Deactivating or deleting a department that is a default is refused on the Departments tab, naming where it is used: "HR is the default department for new tickets for Acme and the install. Choose a replacement first." The dialog offers, per place, a replacement department or No default - and choosing No default for a company where Department is hidden shows the warning above before it is saved. The default is never cleared behind the administrator's back.
Built (4.9.0). As designed, with two simplifications: the Departments tab refuses a deactivate or delete with a message pointing at the Department field panel, rather than offering a replacement in its own dialog; and the Custom fields tab keeps its name (renaming it to Fields would have touched every translation for little gain). When no company on the board shows Department, the Department folder grouping and table column go as well, and the folders show the Analyst grouping (in memory only). Checked on a test install made multi-company: one company hiding it (that company's tickets lose the field, the right-click item and the New ticket box; the others keep them), every company hiding it (grouping falls back, bulk button and column go), and the control with it shown (everything back, the saved grouping returns).
Mandatory fields stays as it is. MandatoryFieldsService::fieldAvailable() gains no Department case. Guard 1 makes "required and hidden" impossible, so the existing behaviour - a ticket with no department cannot close cleanly when Department is required, and the analyst can fill it in because the field is shown - holds unchanged.
2.3 Company
FreeITSM already gets most of this right. The Company row on New ticket appears only in the all-companies view, when the analyst can reach more than one company. Inside one company it is never asked. There is no new setting to hide it. Instead, three gaps found while reading the code are fixed:
- The server does not enforce the choice. In the all-companies view, a create request with no company falls back to the session's company, or the first one the analyst can reach. Only the browser stops that. The server will refuse it: "Choose which company this ticket is for." No silent fallback to the first company, ever.
- One reachable company is not ambiguous. An analyst in the all-companies view who can reach just one company is not asked, and the ticket goes to that company. The server will make that explicit, rather than relying on the same fallback.
- The form does not follow the chosen company. The mailbox list loads for the active company, not the one chosen on the form, and custom fields can belong to one company. Changing the Company choice will reload the mailbox list and the custom field questions for that company, keeping answers to fields both companies share.
A personal preference: My company for new tickets, under System -> Preferences. In the all-companies view it pre-selects that company, and the row stays visible so you can see where the ticket is going. It is a convenience, not an authority: each time the form opens it is checked against the companies you can reach now, and if you no longer can, nothing is pre-selected and you choose. The server checks the submitted company as it would any other.
Built (4.9.0). All three gaps closed (create refuses a missing company, or uses the only reachable one; mailboxes take ?tenant_id=, custom fields already did), and the tickets_new_ticket_company preference pre-selects, re-checked against the reachable list on every opening. Checked on a test install made multi-company for the purpose: refused with no company; mailboxes follow the chosen company and ignore an unreachable one; the preference pre-selects and an unreachable one pre-selects nothing. See All companies ticket view.
2.4 Contractor fields on tasks
Setting: Contractor fields on tasks: Show / Hide, Tasks -> Settings -> General, per company, default Show. As built (4.9.0): on Tasks -> Settings -> Involved, beside the other people on a task.
The name says what it does - it tidies the task screen. The help text says what it does not do: "This only hides the Contractor and Contact there fields on the task screen. Projects and the REST API can still assign contractors. To switch contractors off entirely, turn off the Contracts module." (The request called it Use external contractors; the reviewer asked that the label make the screen-only scope clear, so it is not read as switching external work off across the install.)
When set to Hide:
- The Contractor and Contact there pair disappears from the task panel, together and with no gap. The assigned analyst stays.
- The same applies to the task opened inside a ticket and on a phone, which share the same code.
- The sidebar Contractor filter is hidden.
- A task that already has a contractor still shows it, read-only, with a note: "Contractor fields are hidden. This one stays recorded." Saving anything else on the task never touches it.
Built (4.9.0). Key tasks_contractor_fields through taskContractorFieldsOn() (per company over the install default, default on); api/tasks/contractor_fields.php reads and saves the default and the per-company table under the Involved tab's permission; api/tasks/get.php sends contractor_fields with each task, resolved for the task's company (no company = the Default company); window.TASK_CONTRACTOR_FIELDS (the active company, or the install default in the all-companies view) hides the sidebar filter. Checked on a test install: shown, the dropdowns; hidden, a contractor already on the task shows read-only with the note and no dropdowns.
Not changed:
- Projects. A contractor can be a project member, and the AI assistant can assign one.
- The REST API. It still reads and writes contractors.
- Module availability (#217). This is not a #217 feature switch, because nothing is refused - only hidden. Switching the Contracts module off already removes contractor choosing entirely, since contractors are Contracts suppliers.
2.5 Acceptance criteria, against Part A
| # | Criterion | How |
|---|---|---|
| 1 | Configurable without disabling a module | Department and contractor switches, per company |
| 2 | Company omitted only when safely determined | Already true in a single company. All-companies is enforced on the server; a preference only pre-selects (2.3) |
| 3 | Department omitted without hidden-required failures or routing changes | Hidden-while-required refused both ways; routing unchanged; default applied only at creation (2.2) |
| 4 | One contractor switch, assignee stays | 2.4 |
| 5 | Hidden values survive edits and re-enabling | Patch-style saves, proved by tests |
| 6 | Consistent everywhere, no gaps | The lists in 2.2 and 2.4 |
| 7 | Permissions kept; invalid defaults explained | Inactive defaults refused; deactivating a default needs a replacement or an explicit No default; a stale default is loud at creation |
| 8 | Verification | Section 7 |
3. Part B - a calmer task Work view
3.1 What changes
When a task's Work view opens, each of Checklists, Forms and Follow-ups is drawn in one of three states:
| State | Shown as |
|---|---|
| Loading | Not shown yet, as today. Never "(0)". (Changed while building: the plan was a heading with a spinner, but a section only knows whether it is switched on once it has loaded, so a spinner would flash headings for sections that then disappear.) |
| Could not load | The heading, open, with "Could not load checklists" and a Retry button. Never "(0)", never hidden |
| Loaded | Has records: open, as today. None: Checklists (0), collapsed |
Today, a section that fails to load simply disappears, which hides the failure. Part B makes that visible - a small fix in its own right.
"Empty" means no records at all. A completed checklist, a submitted form or a closed follow-up counts as a record, so its section stays open.
The heading is a button with a chevron and aria-expanded, reusing the subtask disclosure already on the task cards. It works by click, Enter and Space. Opening it shows exactly the controls shown today (attach a checklist, add a form, create or link a follow-up), with the same permission checks.
3.2 Keeping it steady
- Your choice sticks while the task is open. Open an empty section, and it stays open through every redraw of that task - adding a checklist step, a form saved in another tab, completing a subtask, changing the project. Each redraw keeps the open/closed state for that task, the same way the inline forms already keep which ones are open.
- Adding the first item keeps it open, so you can see what you added.
- Follow up from a failed checklist step (or a form) opens Follow-ups, scrolls to it and puts the cursor in the title box, as it does now - but now also when the section was collapsed.
- Half-typed text survives. Some redraws today rebuild the follow-up and checklist-note boxes, which loses what you were typing. Those redraws will carry the typed text across.
3.2a How it was built (4.9.0)
workSectionHead()inassets/js/tasks.jsdraws each section's heading: the plain label, or the fold button when the preference is on and the section is empty. The three render functions share it.- The open/closed state (
workFold) and typed drafts (workDrafts) are kept outside the panel's markup, keyed by task, becauseopenDetailPanel()rebuilds the whole panel after many actions. A section seen with records counts as opened, so removing its last record does not fold it. - Drafts are recorded on
inputfor the follow-up title and reference, step values and Fail notes, put back after each redraw, and dropped once sent.followUpFrom()opens Follow-ups and drops a stale title draft. - A failed load calls
workSectionFailed(): the heading, the error and Retry, reported to the Work view as switched on so the block does not vanish. - Preference key
tasks_collapse_empty_work('on'or empty), read bytasks/index.phpintowindow.TASK_COLLAPSE_EMPTY_WORK- so a task opened inside a ticket, which is the same page, follows it too.
Checked in a browser on a test install, through the real Tasks page: first load folded; open keeps focus on the heading; open state and a typed title survive a full panel redraw; Follow up opens a folded Follow-ups; a failed load shows Retry and recovers; attaching a checklist shows it in full and removing it leaves it open; the next task starts folded; preference off looks as before.
3.2b Procedures joins (PR #247, 4.10.0)
This section named three sections, so 4.9.0 left Procedures - the fourth section in the Work view - showing in full, and its loader still hid the section when it failed. PR #247 by Dr4inder made Procedures the fourth caller of workSectionHead() and workSectionFailed(), and kept a half-typed procedure search across a redraw. One fix followed the merge: adding a procedure now clears that search, as every other draft is cleared once sent. No new setting; the same preference covers all four.
3.3 Who decides
A user preference: Collapse empty work sections, under System -> Preferences -> Tasks, default off, so the Work view looks as it does today until you choose otherwise. No install-wide default (agreed). As built (4.9.0): the preference is headed Empty work sections on a task.
It applies everywhere the Work view is drawn: the Work tab, the wide column, the side panel, and a task opened inside a ticket.
4. Part C - portal questions in the main form
4.1 What changes
A new setting, Where the portal asks custom field questions: Side panel / Main form, default Side panel. It goes on Tickets -> Settings -> Fields, beside the existing Where requesters see their answers setting. It is install-wide, like every other portal setting (agreed). As built (4.9.0): on the Custom fields tab.
With Main form, the New ticket page reads top to bottom:
Subject -> Request type (when it matters) -> Custom field questions -> Description
- The type picker moves with the questions, because it decides which ones are asked. It still only appears when choosing a type matters, as today.
- The questions keep their order, labels, hints and required markers. They use the same renderer, so validation and storage are unchanged.
- Answers are never lost: changing the type keeps any answers that still apply, as it does now, and a validation error on submit leaves every answer, the subject and the description as they were.
- The side panel keeps everything else: mailbox, category, priority, the Confidential toggle, equipment, attachments, and Submit.
4.2 The layout change underneath
Today the page is built so that it never scrolls. The description editor stretches to fill whatever height is left, and only the side panel scrolls. That is why the questions live in the side panel: in the main column they would be squeezed by the editor.
With Main form, the main column scrolls instead. The description editor gets a fixed, comfortable height (about 300px, the same idea as the ticket note box) rather than filling the space. So several questions, long answers and error messages all stay readable. On a phone the page is already one column, and the editor's Full screen button still works.
On a narrow screen, Submit stays reachable. On a phone the side panel stacks under the main form; the questions, the description and Submit must all be reachable by scrolling, with nothing pinned over them. Checked at 390px with several required questions and a validation error showing.
With Side panel, the page is exactly as it is today.
4.3 How it was built (4.9.0)
- Setting
ticket_portal_field_ask(side- never saved - ormain), read byTicketFieldsService::portalAskPlace(), saved through the Custom fields tab liketicket_portal_field_answersand listed beside it in the settings manifest. self-service/new-ticket.phpreads it before the portal header, because it decides the body class (ss-ask-main). The type picker and the question box are printed by one function in either place, with the same ids, so the script that loads, keeps and submits the answers is untouched.- With
main, the questions are the first row of the left column; the page scrolls instead of the app shell; the editor is 320px; below 900px everything stacks in DOM order. - Found while testing: with the page scrolling, an error from Submit appeared above the form, out of sight of someone who had scrolled down to Submit. Both error paths now scroll it into view (harmless with the side panel).
Checked in a browser on a test install with four sample questions: both placements at 1400px and 520px; a refused submit keeps the subject and every answer and shows the error in view; Side panel is pixel-identical to the page before the change.
5. Part D - custom fields for chosen departments (deferred)
Not part of #218. The reviewer agreed it may be useful but is not needed for this request, and asked that A to C not depend on it. It will be discussed as a separate follow-up once A to C are in. The outline is kept here so it is not lost.
The idea. The custom field dialog already has Which ticket types: All / Only these. It would get a matching Which departments: All / Only these. A field appears when both match. A field set to All departments behaves exactly as today.
What a future design must settle (from the review):
- Department applicability is not confidentiality or access. Limiting a field to HR decides which questions are asked, never who may see the ticket.
- No unanswerable required question. A field limited to departments, set to Ask the requester and Required on create, must not become a required portal question that the portal never shows, because the requester does not choose a department. That includes the case where Part A's default department is known at creation.
- A department change on an existing ticket must not erase old answers, and must not trap unrelated edits behind creation-only requirements that now apply.
Storage, if built: ticket_field_departments (field_id, department_id) beside ticket_field_types, an all_departments column on ticket_fields (default 1), and the department passed to TicketFieldsService::fieldsFor().
6. What is not in scope
- A general form or page designer. The request says so explicitly.
- Part D (section 5).
- Hiding other built-in fields (priority, type, category...). Category, category at close and resolution code already have their own switches. Others can follow the same pattern later if asked for.
- Making departments per company. They are shared by every company today, and changing that touches teams, SLAs and routing.
- Switching contractors off across the install. That is #217's job, and switching the Contracts module off already does it.
- Any change to who can see or do what.
7. How it will be tested
- Part B: each section loaded empty, loaded with records (including only completed or closed ones), failed to load and slow to load. Open a section, then trigger each kind of redraw and check it stays open and half-typed text survives. Follow up from a failed step with Follow-ups collapsed. The same in a task opened inside a ticket. Keyboard only.
- Part C: both placements on desktop and at 390px, with several required questions, a type change and a validation error; every answer survives, and Submit is reachable.
- Part A:
- Create a ticket on every channel with Department hidden, with and without a default. An explicit department wins over the default; an invalid explicit one is refused, not defaulted.
- Edit an existing ticket that has a hidden department, and check it is still there. Edit an existing ticket with none, and check the default is not applied.
- Hiding Department while it is required at closure is refused; ticking it on Mandatory fields while hidden is refused.
- Deactivating and deleting a default department are refused until a replacement or No default is chosen. A default made inactive behind the screen's back is not applied at creation, is written in the ticket's history and is flagged on the panel.
- The all-companies view refuses a create with no company; one reachable company is used explicitly; changing company reloads mailboxes and custom fields; a preference for a company you can no longer reach pre-selects nothing.
- A task with a contractor survives an unrelated save with contractor fields hidden, and shows it read-only.
- Switch everything back on and check every value reappears.
8. The reviewer's answers, and what they changed
@Dr4inder answered the eight questions on 2026-10-29.
| # | Question | Answer | Change to this page |
|---|---|---|---|
| 1 | Default department on every channel? | Yes - portal, email and chat too, when nothing else set one. Keep a department an authorised channel or caller supplied; do not replace an invalid explicit choice with the default; resolve it before department-dependent questions and create-time validation; never on edits | 2.2 The default department |
| 2 | Hidden Department with no default? | Acceptable when departments are unused, with the team-visibility warning. But hiding must not make a closure rule stop checking Department - refuse the conflict instead. Old tickets with no department must still be fixable. Deactivating or deleting a default must not silently clear it; revalidate the default at creation | Changed. The Mandatory fields skip is gone; hidden-while-required is refused both ways; deactivating a default needs a replacement or explicit No default; a stale default is loud at creation (2.2 Guards) |
| 3 | Company preference: pre-select, or hide? | Pre-select is enough. It is a convenience, not authority - check current access, refresh company-dependent mailboxes and fields when the company changes, no fallback to the first company | 2.3: preference rechecked each time; mailboxes and custom fields reload on company change |
| 4 | Existing contractor when switched off? | Visible and read-only. Make the setting's screen-only scope explicit in its label and help, since REST and Projects still use contractors | 2.4: renamed Contractor fields on tasks: Show / Hide, with help text naming what it does not do |
| 5 | Collapse empty sections: personal or install-wide? | Personal, default off is enough | None |
| 6 | Portal placement per company? | Install-wide is enough, Side panel by default. Keep answers through validation errors and type changes, and keep the questions and Submit reachable on a narrow screen | 4.1 and 4.2 |
| 7 | Part D? | Potentially useful, not needed for this request - defer it, and keep A to C independent of it. Conditions for a future design listed | Section 5 marked deferred, with the conditions |
| 8 | Order? | B and C first, then A, each independently deliverable | Status and the table at the top |
See also: Custom ticket fields · Tickets settings - developer guide · Multi-tenancy settings · Task work view · Self-service portal · Module availability - design (#217)