{"evidenceTier":"self-signed","executor":{"implName":"manual-demand-test","implVersion":"1","mode":"manual","model":"claude-sonnet-5 (solve), reviewed"},"generatedAt":"2026-07-28T11:06:12.881Z","participant":{"agentEoa":"0x654862876C8Ddaa048Fa9E0D32D5e0e81bD4Bd20"},"payload":{"branch":"feat/unlockable-name-field-12330","commitSha":"c71bbbac2","patchDiff":"From c71bbbac2eb80ef2042044afd84bc9ec94128304 Mon Sep 17 00:00:00 2001\nFrom: Oaksprout <oaksproutthetan@gmail.com>\nDate: Tue, 28 Jul 2026 12:00:34 +0100\nSubject: [PATCH] feat: make locked name/brand/operator fields unlockable\n (#12330)\n\nFields get locked for editing when the underlying entity carries a\n`wikidata` tag (or is a suggestion preset without a brand/operator\nfield), so users can't accidentally overwrite verified data. Until\nnow the lock icon shown next to the field label was purely\ninformational - the only way around it was to edit the raw tags\nmanually.\n\nPer tyrasd's proposed design in #12330 (originally #8892), turn the\nlock icon into a real button: clicking it unlocks the field for the\nrest of the editing session, while still surfacing the \"why\" via the\nexisting tooltip before the user commits to editing. The icon is a\nnative <button>, matching the existing remove/revert icon pattern, so\nit's keyboard-actionable and gets the same inspector-hover treatment\nfor free.\n\nAlso updates the lock tooltip copy to explain the new interaction\ninstead of pointing at the raw tag editor.\n\nmodules/ui/field.js is the single place that renders the lock icon\nfor every locked field type (name via fields/localized.js, and\nbrand/network/operator/flag via fields/input.js), so this fixes the\nissue for all of them without touching either field implementation.\n---\n css/80_app.css        | 16 +++++++++\n data/core.yaml        |  3 +-\n modules/ui/field.js   | 42 +++++++++++++++++-----\n test/spec/ui/field.js | 83 +++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 134 insertions(+), 10 deletions(-)\n create mode 100644 test/spec/ui/field.js\n\ndiff --git a/css/80_app.css b/css/80_app.css\nindex 48124c35b..3c82a7bff 100644\n--- a/css/80_app.css\n+++ b/css/80_app.css\n@@ -1479,6 +1479,22 @@ button.preset-reset .label.flash-bg {\n     fill: var(--text-color);\n }\n \n+/* the lock icon is a toggle button, not a boxed field-label action button - #12330 */\n+.field-label .label-textannotation .lock-icon {\n+    display: inline-flex;\n+    align-items: center;\n+    width: auto;\n+    border: 0;\n+    border-radius: 0;\n+    padding: 0;\n+    background: none;\n+    cursor: pointer;\n+}\n+.field-label .label-textannotation .lock-icon:hover .icon,\n+.field-label .label-textannotation .lock-icon:focus .icon {\n+    opacity: .8;\n+}\n+\n .field-label .modified-icon,\n .field-label .remove-icon,\n .field-label .remove-icon-multilingual {\ndiff --git a/data/core.yaml b/data/core.yaml\nindex 6795ba39f..c9b631ceb 100644\n--- a/data/core.yaml\n+++ b/data/core.yaml\n@@ -19,6 +19,7 @@ en:\n     expand: expand\n     collapse: collapse\n     plus: add\n+    unlock: unlock\n   toolbar:\n     inspect: Inspect\n     undo_redo: Undo / Redo\n@@ -768,7 +769,7 @@ en:\n     location: Location\n     add_fields: \"Add field:\"\n     lock:\n-      suggestion: 'The \"{label}\" field is locked because there is a Wikidata tag. You can delete it or edit the tags in the \"Tags\" section.'\n+      suggestion: 'The \"{label}\" field is locked because there is a Wikidata tag. Click the lock icon to unlock it for editing.'\n     display_name_addr: \"{housenumber} {streetOrPlace}\"\n     display_name_addr_with_unit: \"{unit}, {housenumber} {streetOrPlace}\"\n     display_name:\ndiff --git a/modules/ui/field.js b/modules/ui/field.js\nindex 79457e4b7..3fb50441a 100644\n--- a/modules/ui/field.js\n+++ b/modules/ui/field.js\n@@ -38,6 +38,8 @@ export function uiField(context, presetField, entityIDs, options) {\n     }\n \n     var _locked = false;\n+    var _lockOverridden = false;   // true once the user has clicked the lock icon to unlock the field\n+    var _selection = d3_select(null);\n     var _lockedTip = uiTooltip()\n         .title(() => t.append('inspector.lock.suggestion', { label: field.label() }))\n         .placement('bottom');\n@@ -153,7 +155,24 @@ export function uiField(context, presetField, entityIDs, options) {\n     }\n \n \n+    // Unlocks the field so its value(s) can be edited. Clicking the lock icon\n+    // acknowledges the Wikidata-tag warning shown in the tooltip; the field\n+    // then behaves like any other unlocked field for the rest of this\n+    // editing session (see https://github.com/openstreetmap/iD/issues/12330).\n+    function unlock(d3_event) {\n+        d3_event.stopPropagation();\n+        d3_event.preventDefault();\n+        if (!_locked) return;\n+\n+        _lockOverridden = true;\n+        _locked = false;\n+        field.render(_selection);\n+    }\n+\n+\n     field.render = function(selection) {\n+        _selection = selection;\n+\n         var container = selection.selectAll('.form-field')\n             .data([field]);\n \n@@ -271,19 +290,22 @@ export function uiField(context, presetField, entityIDs, options) {\n                 .classed('present', tagsContainFieldKey());\n \n \n-            // show a tip and lock icon if the field is locked\n+            // show a tip and a toggleable lock icon if the field is locked\n             var annotation = container.selectAll('.field-label .label-textannotation');\n-            var icon = annotation.selectAll('.icon')\n+            var lockButton = annotation.selectAll('.lock-icon')\n                 .data(_locked ? [0]: []);\n \n-            icon.exit()\n+            lockButton.exit()\n                 .remove();\n \n-            icon.enter()\n-                .append('svg')\n-                .attr('class', 'icon')\n-                .append('use')\n-                .attr('xlink:href', '#fas-lock');\n+            var lockButtonEnter = lockButton.enter()\n+                .append('button')\n+                .attr('class', 'lock-icon')\n+                .attr('title', t('icons.unlock'))\n+                .call(svgIcon('#fas-lock'));\n+\n+            lockButtonEnter.merge(lockButton)\n+                .on('click', unlock);\n \n             container.call(_locked ? _lockedTip : _lockedTip.destroy);\n     };\n@@ -314,7 +336,9 @@ export function uiField(context, presetField, entityIDs, options) {\n \n     field.locked = function(val) {\n         if (!arguments.length) return _locked;\n-        _locked = val;\n+        // once the user has unlocked the field, keep it unlocked regardless\n+        // of what the lock computation says on subsequent renders\n+        _locked = val && !_lockOverridden;\n         return field;\n     };\n \ndiff --git a/test/spec/ui/field.js b/test/spec/ui/field.js\nnew file mode 100644\nindex 000000000..75de837df\n--- /dev/null\n+++ b/test/spec/ui/field.js\n@@ -0,0 +1,83 @@\n+import { select as d3_select } from 'd3-selection';\n+\n+describe('iD.uiField', function() {\n+    var context, selection;\n+\n+    beforeEach(function() {\n+        context = iD.coreContext().assetPath('../dist/').init();\n+        selection = d3_select(document.createElement('div'));\n+    });\n+\n+    // Builds a `name` field (type `localized`) that gets locked because the\n+    // entity carries a `wikidata` tag - see modules/ui/fields/localized.js\n+    // `calcLocked` and https://github.com/openstreetmap/iD/issues/12330.\n+    function createNameField(tags) {\n+        var entity = new iD.osmNode({ id: 'n1', tags: tags });\n+        context.history().merge([entity]);\n+        var presetField = iD.presetField('name', { key: 'name', type: 'localized' });\n+        var field = iD.uiField(context, presetField, [entity.id]);\n+        field.tags(entity.tags);\n+        return { field: field, entity: entity };\n+    }\n+\n+    function clickLockIcon() {\n+        selection.selectAll('.lock-icon').node()\n+            .dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));\n+    }\n+\n+    it('locks the name field and shows a lock icon when the entity has a wikidata tag', function() {\n+        var field = createNameField({ name: 'Foo', wikidata: 'Q1' }).field;\n+        selection.call(field.render);\n+\n+        expect(field.locked()).toBeTruthy();\n+        expect(selection.selectAll('.form-field').classed('locked')).toBeTruthy();\n+        expect(selection.selectAll('.field-label .lock-icon').size()).toEqual(1);\n+        expect(selection.selectAll('.localized-main').attr('readonly')).toEqual('true');\n+    });\n+\n+    it('does not lock or show a lock icon when there is no wikidata tag', function() {\n+        var field = createNameField({ name: 'Foo' }).field;\n+        selection.call(field.render);\n+\n+        expect(field.locked()).toBeFalsy();\n+        expect(selection.selectAll('.form-field').classed('locked')).toBeFalsy();\n+        expect(selection.selectAll('.field-label .lock-icon').size()).toEqual(0);\n+        expect(selection.selectAll('.localized-main').attr('readonly')).toBeNull();\n+    });\n+\n+    it('renders the lock icon as a real button so it is keyboard-actionable, matching remove/revert icons', function() {\n+        var field = createNameField({ name: 'Foo', wikidata: 'Q1' }).field;\n+        selection.call(field.render);\n+\n+        expect(selection.select('.field-label .lock-icon').node().tagName).toEqual('BUTTON');\n+    });\n+\n+    it('unlocks the field for editing when the lock icon is clicked', function() {\n+        var field = createNameField({ name: 'Foo', wikidata: 'Q1' }).field;\n+        selection.call(field.render);\n+\n+        clickLockIcon();\n+\n+        expect(field.locked()).toBeFalsy();\n+        expect(selection.selectAll('.form-field').classed('locked')).toBeFalsy();\n+        expect(selection.selectAll('.field-label .lock-icon').size()).toEqual(0);\n+        expect(selection.selectAll('.localized-main').attr('readonly')).toBeNull();\n+    });\n+\n+    it('stays unlocked across later re-renders of the same field in this session', function() {\n+        var setup = createNameField({ name: 'Foo', wikidata: 'Q1' });\n+        var field = setup.field;\n+        selection.call(field.render);\n+\n+        clickLockIcon();\n+        expect(field.locked()).toBeFalsy();\n+\n+        // a later tag edit re-renders the field (as happens on every keystroke) -\n+        // the wikidata tag is still present, but the field must not re-lock\n+        field.tags(setup.entity.tags);\n+        selection.call(field.render);\n+\n+        expect(field.locked()).toBeFalsy();\n+        expect(selection.selectAll('.field-label .lock-icon').size()).toEqual(0);\n+    });\n+});\n-- \n2.50.1 (Apple Git-155)\n\n","testEvidence":"2368/2368 in node:22 container (build then network-off tests) incl. 5 new field.js tests; eslint+tsc+build clean solve-side"},"role":"solution","schemaVersion":"jinn.execution.v1","signature":{"algo":"secp256k1","hash":"0xa504a0dc8385c4ae2e9e71f2f3f12d7bcf7701cfdf470b4dac29b85514d44615","sig":"0xd8513b5887186d96c0498e4a81542bec9dba1a6503bd05f0934b69e1e88d3997096f7b4f62140eb6b4addf9aa5e432b3cccb4d67cdbf907594d665b05e4d084c00","signer":"0x654862876C8Ddaa048Fa9E0D32D5e0e81bD4Bd20"},"solverType":"external-repo-fix.manual.v1","task":{"baseCommit":"87376a46b56edecdfce8e44c01731c9403de7b51","cid":"bafkreidk7qufl3xqygkkzpuw7v226qxnr5r2msqbpvoe5xrrc3ubqjfv7y","onchainCreationTx":"0x5c6b41160cba714851f68099cc721949d7b5b3e2f675d4f02d699959ed0ec77f","repo":"openstreetmap/iD","requestId":"0xc0366a258890183df9e15d5b969f55389ed134eb146ff692c6054e044416df50"}}