Repository navigation
Task Issue 954 Cfields Used Type Lock
Task 954 — Forbid type/node-type change of custom fields already holding values (cfieldsView + cfieldsAssignView)
Issue: #954
Status: IMPLEMENTED & VERIFIED (2026-09-22) — branch task/issue-954-cfields-used-type-lock
Legacy edit of a custom field that already holds values locks both "Available for" (node type) and "Type" as read-only static text:
-
gui/templates/dashio/cfields/cfieldsEdit.tpl:105-141— when$gui->cfield_is_usedthe template renders the selected node-type/type labels as plain text plus hidden inputscf_node_type_id/cf_typethat carry the original values, so ado_updatecan never change them; -
lib/cfields/cfieldsEdit.php:37,280computescf_is_usedviacfieldMgr->is_used()(lib/functions/cfield_mgr.class.php:1486), which returns 1 as soon as the field has a row in any of the four value tables (cfield_design_values,cfield_build_design_values,cfield_testplan_design_values,cfield_execution_values).
The modern screens dropped this: the shared edit modal
(cfieldsView.html and cfieldsAssignView.html) always rendered editable
Type + Node Type combos for used fields, and PUT /api/cfields/{id}
(api/cfields/index.php:191-204 at fix time) wrote the submitted values
straight into the DB — a used field could be silently re-typed.
Measured repro (fixture: Tier cf with one cfield_design_values row,
is_used=1):
-
GET /api/cfields/index.php/1→{... type:0, node_type_id:3}— nois_usedexposed; -
PUT /api/cfields/index.php/1 {type:8, node_type:'testcase'}→ HTTP 200, re-GET showstype=8— silent re-type.
-
usedFieldIds()(helper,index.php:76-90): resolves once per request the set of field ids that appear in any of the four value tables — a single UNION query replacing oneis_used()SQL per row on list endpoints. Table names come fromtlObject::getDBTables([...])(cfield_mgr->tablesisprotected,lib/functions/object.class.php:78). -
cfToJSON($cf, $isUsed)now emits'is_used' => 1|0on every payload: GET list (:135), GET /{id} (:149), POST create result (:223), PUT result (:309), and GET /assignment linked + available rows (:408,:423). -
PUT /{id} guard (
:269-288): whenis_used, the submittedtype/node_typeare normalised to int (intval— the node map returns DB strings like'3', which would have tripped the strict!==compare) and compared against the stored values. If either genuinely differs → HTTP 400{status:'error', code:'warning_no_type_change', message: lang_get('warning_no_type_change', assignLocale())}(the legacy localized warning). If unchanged (the normal UI save always sends the preserved values) the attributes are force-preserved from$existingwhile every other attribute (label, possible values, flags) saves normally.
- Modal adds
#editTypeText/#editNodeTypeTextstatic-text elements and a#typeLockWarningbanner (cf.msg.warningNoTypeChange, hidden by default). - New
setTypeEditable(editable)toggles selects ↔ static text + warning. -
editCf()fills the static text (typeMap[cf.type], resolved node name) and callssetTypeEditable(!cf.is_used);showCreateModal()(list screen) resets to editable mode. -
saveCf()error mapping: BFF responsecode === 'warning_no_type_change'→ the localized bundle textcf.msg.warningNoTypeChange. - The assign screen's name links in both the Assigned and Available tables
use the same
editCf(), so the lock applies everywhere the field is editable.
cf.msg.warningNoTypeChange added to all 10 bundles
(gui/templates/i18n/{en,ro,de,fr,es,it,pt,ja,zh,ru}.json), wording adapted
from the legacy $TLS_warning_no_type_change
(locale/en_US/strings.txt:816, de_DE:722, fr_FR, es_ES:775,
pt_PT:845, ja_JP:884, zh_CN); ro/it had no legacy translation and get
a fresh faithful translation. All bundles validated with python3 -m json.tool.
-
cfieldsView, used field (Tier): modal shows Type
string+ Node Typetestcaseas static text (selectsdisplay:none) + warning banner. - cfieldsView, value-less field (TierEmpty): selects editable, no banner.
-
Normal save on used field (label change): toast "Custom field saved",
label persisted,
type/node_type_iduntouched, no modal error. -
BFF backstop:
PUT {type:4}andPUT {node_type:'build'}→ HTTP 400code:warning_no_type_change; stored values unchanged. - cfieldsAssignView: used field locked from the name link in the Assigned table and, after an unlink, from the Available table (verified, then re-linked).
- Non-used field re-type still allowed (no scope regression).
-
German locale (
?locale=de): banner localisedDieses benutzerdefinierte Feld enthält bereits Werte; …. - Event Viewer/
events: 18 rows, alllog_level=16(audit INFO), 0 rows > 16 (no Error/Warning). - Browser console clean (only pre-existing a11y hints); inline JS
node --checkclean; all 10 bundles valid.
Suite 954 in tmp/TLU_Test_Cases.md — 9/9 PASS.

Refs #954.