From a1bbf172aad8443bfbb1859760d62669d6705189 Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Sun, 4 Oct 2026 12:00:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=BE=A9=E5=8E=9F=E7=94=9F=E9=81=B8?= =?UTF-8?q?=E5=96=AE=20mut=20=E7=B6=81=E5=AE=9A=E4=B8=A6=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E6=88=90=E5=93=A1=E5=84=B2=E5=AD=98=E6=AC=8A=E9=99=90=E9=82=8A?= =?UTF-8?q?=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- COMPATIBILITY.md | 10 +++ README.md | 11 ++++ .../form-balancer-rules/template.hbs | 4 +- .../machine/driver-amazonec2/template.hbs | 4 +- .../machine/driver-digitalocean/template.hbs | 6 +- .../machine/driver-exoscale/template.hbs | 6 +- .../machine/driver-packet/template.hbs | 6 +- .../machine/driver-rackspace/template.hbs | 4 +- .../machine/driver-ubiquity/template.hbs | 4 +- .../modal-edit-setting/template.hbs | 2 +- app/components/schema/input-enum/template.hbs | 2 +- .../schema/input-secret/template.hbs | 2 +- app/components/view-edit-project/template.hbs | 2 +- ...ass-replacement.node24-ignore-scripts.json | 4 +- docs/releases/web-console-1.6.176.md | 35 +++++++++++ package-lock.json | 4 +- package.json | 2 +- scripts/check-modernization-blockers | 6 +- scripts/check-ui-console-workspace | 2 +- scripts/check-ui-critical-high-dependencies | 2 +- .../view-edit-project-permissions-test.js | 61 ++++++++++++++++++- .../helpers/legacy-template-action-test.js | 23 ++++++- 22 files changed, 168 insertions(+), 34 deletions(-) create mode 100644 docs/releases/web-console-1.6.176.md diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 36f0581f25..f9a970d87c 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -4,6 +4,16 @@ Web Console preserves compatible API paths, schema and resource names, action na Visible branding, product-owned assets, icon identifiers, package metadata, and operator documentation use PastureStack. Historical identifiers remain only where they are server data or protocol contracts and must not be mechanically replaced. +Candidate `1.6.176` preserves native `mut` setters by composing `action` with +`fn (mut ...)` in the same 21 existing select bindings. The classic helper's +event `target.value` conversion is unchanged. Model identities, selected-row +references, schema choices, member capability checks, metadata/network writes, +save ownership/finalizers, API payload contracts and authentication are unchanged. +The shared compatibility helper and dependency graph are unchanged. Exact-source +CI, publication and packaged UI acceptance are pending; the full matrix remains +INCOMPLETE and historical HOLDs remain immutable. +See the [release note](docs/releases/web-console-1.6.176.md). + Published `1.6.175` refreshes only the image form's locally owned validation aggregate after image/locale correction. Other validation errors and backend save failures are preserved; shared NewOrEdit ownership, hook/finalizer behavior, diff --git a/README.md b/README.md index d973bf132a..2d161cde10 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,17 @@ PastureStack is an independent community effort to preserve, audit, and moderniz ## Project status +Candidate `1.6.176` repairs native select bindings that passed a `mut` reference +directly to the classic compatibility `action` helper. The helper receives the +current value instead of a setter; wrap `mut` in native `fn` to retain the setter +while preserving the existing event value mapping. This covers the same 21 +select bindings in 11 templates, including project roles, schema fields, +balancer rules, settings and machine-driver fields. Project member capabilities, +identities, metadata/network write boundaries, save finalizers and authentication +remain unchanged. Exact-source CI, immutable publication and packaged QA +acceptance are pending; historical HOLDs are not promoted and the full matrix +remains INCOMPLETE. See the [release note](docs/releases/web-console-1.6.176.md). + Published `1.6.175` keeps the container/VM form's image validation message in sync when the operator corrects the image or changes the locale. Only the form's own validation aggregate is refreshed: model/command errors and later diff --git a/app/components/form-balancer-rules/template.hbs b/app/components/form-balancer-rules/template.hbs index 58f8913eef..7725576112 100644 --- a/app/components/form-balancer-rules/template.hbs +++ b/app/components/form-balancer-rules/template.hbs @@ -61,7 +61,7 @@ - @@ -78,7 +78,7 @@ {{/if}} - {{#each this.protocolChoices as |proto|}} {{/each}} diff --git a/app/components/machine/driver-amazonec2/template.hbs b/app/components/machine/driver-amazonec2/template.hbs index d6148be6d3..5d019f03cd 100644 --- a/app/components/machine/driver-amazonec2/template.hbs +++ b/app/components/machine/driver-amazonec2/template.hbs @@ -11,7 +11,7 @@
{{#if this.isStep1}} - {{#each this.regionChoices as |choice|}} {{/each}} @@ -77,7 +77,7 @@
{{#if this.isStep3}} - {{#each this.zoneChoices as |choice|}} {{/each}} diff --git a/app/components/machine/driver-digitalocean/template.hbs b/app/components/machine/driver-digitalocean/template.hbs index e4e86cce99..ec792dfa3e 100644 --- a/app/components/machine/driver-digitalocean/template.hbs +++ b/app/components/machine/driver-digitalocean/template.hbs @@ -38,7 +38,7 @@
- {{#each this.regionChoices as |choice|}} {{/each}} @@ -55,7 +55,7 @@
- {{#each this.imageChoices as |choice|}} {{/each}} @@ -65,7 +65,7 @@
- {{#each this.filteredSizeChoices as |choice|}} {{/each}} diff --git a/app/components/machine/driver-exoscale/template.hbs b/app/components/machine/driver-exoscale/template.hbs index 0a566c5fd8..2138488bd3 100644 --- a/app/components/machine/driver-exoscale/template.hbs +++ b/app/components/machine/driver-exoscale/template.hbs @@ -120,7 +120,7 @@
{{#if this.isCustomSecurityGroup}} - {{#each this.allSecurityGroups as |choice|}} {{/each}} @@ -181,7 +181,7 @@
- {{#each this.allInstanceProfiles as |choice|}} {{/each}} @@ -200,7 +200,7 @@
- {{#each this.allTemplates as |choice|}} {{/each}} diff --git a/app/components/machine/driver-packet/template.hbs b/app/components/machine/driver-packet/template.hbs index 845df4d1c5..d63393a894 100644 --- a/app/components/machine/driver-packet/template.hbs +++ b/app/components/machine/driver-packet/template.hbs @@ -35,7 +35,7 @@
- {{#each this.osChoices as |choice|}} {{/each}} @@ -45,7 +45,7 @@
- {{#each this.planChoices as |choice|}} {{/each}} @@ -62,7 +62,7 @@
- {{#each this.facilityChoices as |choice|}} {{/each}} diff --git a/app/components/machine/driver-rackspace/template.hbs b/app/components/machine/driver-rackspace/template.hbs index 789d923865..90d2d77bc8 100644 --- a/app/components/machine/driver-rackspace/template.hbs +++ b/app/components/machine/driver-rackspace/template.hbs @@ -36,7 +36,7 @@
- {{#each this.regionChoices as |choice|}} {{/each}} @@ -53,7 +53,7 @@
- {{#each this.flavorChoices as |choice|}} {{/each}} diff --git a/app/components/machine/driver-ubiquity/template.hbs b/app/components/machine/driver-ubiquity/template.hbs index 236772ebc9..4a4c795a7a 100644 --- a/app/components/machine/driver-ubiquity/template.hbs +++ b/app/components/machine/driver-ubiquity/template.hbs @@ -91,7 +91,7 @@
- {{#each this.allImages as |choice|}} {{/each}} @@ -101,7 +101,7 @@
- {{#each this.allFlavors as |choice|}} {{/each}} diff --git a/app/components/modal-edit-setting/template.hbs b/app/components/modal-edit-setting/template.hbs index 829a3f3770..0efd2a11b9 100644 --- a/app/components/modal-edit-setting/template.hbs +++ b/app/components/modal-edit-setting/template.hbs @@ -23,7 +23,7 @@
{{else if (eq this.model.kind 'enum')}} - {{#each this.model.options as |choice|}} {{/each}} diff --git a/app/components/schema/input-enum/template.hbs b/app/components/schema/input-enum/template.hbs index af6e9d9477..1f2e5e8d40 100644 --- a/app/components/schema/input-enum/template.hbs +++ b/app/components/schema/input-enum/template.hbs @@ -1,4 +1,4 @@ - {{#each this.field.options as |choice|}} diff --git a/app/components/schema/input-secret/template.hbs b/app/components/schema/input-secret/template.hbs index 822bcb61f8..174b910e81 100644 --- a/app/components/schema/input-secret/template.hbs +++ b/app/components/schema/input-secret/template.hbs @@ -1,4 +1,4 @@ - {{#each this.filtered as |secret|}} diff --git a/app/components/view-edit-project/template.hbs b/app/components/view-edit-project/template.hbs index 0bfc96a23f..8a3548107a 100644 --- a/app/components/view-edit-project/template.hbs +++ b/app/components/view-edit-project/template.hbs @@ -99,7 +99,7 @@ {{member.displayType}} {{#if (and this.showEdit this.canEditMembers)}} - {{#each this.roleOptions as |choice|}} {{/each}} diff --git a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json index e9ea2e5e99..11f7add2ea 100644 --- a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json +++ b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.175", + "version": "1.6.176", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.175", + "version": "1.6.176", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/docs/releases/web-console-1.6.176.md b/docs/releases/web-console-1.6.176.md new file mode 100644 index 0000000000..e9844b59bd --- /dev/null +++ b/docs/releases/web-console-1.6.176.md @@ -0,0 +1,35 @@ +# Web Console 1.6.176 + +Candidate source only. Exact-source CI, signed immutable publication, Server +packaging and deployed UI acceptance are pending. Do not promote historical +Project HOLDs or infer full-matrix acceptance from this correction. + +## Root cause and minimal repair + +The native Project member-role select used `action (mut member.role)` with +`value="target.value"`. Ember 7.2 exposes the current value of an invokable `mut` +reference to a classic helper. The compatibility `action` helper therefore sees +the current role string, not the setter, and dispatches that name instead of +updating the member. The DOM selection can change while the model stays unchanged. + +Wrap the existing reference in native `fn`: `action (fn (mut member.role))`. +The helper now receives a callable setter and retains its existing event value +mapping. Apply exactly the same composition to 21 selects in 11 templates: +Project roles, schema enum/secret fields, settings, balancer rules and the six +affected machine drivers. No shared helper rewrite or unrelated event migration +is introduced. The existing source gate now also detects this unsafe composition +in `{{action ...}}`, not only parenthesized subexpressions. + +Real-component regressions cover plain cloned and EmberObject Project members, +sorted selected-row identity, input/change propagation, unchanged owner/metadata, +members-only native save payload/finalizer and no metadata/network writes. A +rendered native schema enum checks nested model updates without replacing unknown +metadata or schema choices. Existing capability tests retain the unavailable +member-role select when no `setmembers` link exists. + +Dependencies and their graph are unchanged; only root version metadata and its +executable/reviewed-baseline pins become 1.6.176. The Web175 image validation fix, +session generation, save owner, backend schemas, authorization, OIDC, platform +MFA and compatibility contracts remain intact. The complete matrix remains +INCOMPLETE. Retain configuration, volumes and rollback artifacts; this source +change does not authorize company deployment. diff --git a/package-lock.json b/package-lock.json index e9ea2e5e99..11f7add2ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.175", + "version": "1.6.176", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.175", + "version": "1.6.176", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/package.json b/package.json index 2238e09614..bcb5c9638a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pasturestack/web-console", - "version": "1.6.175", + "version": "1.6.176", "private": true, "description": "PastureStack browser console for the compatible control platform.", "repository": { diff --git a/scripts/check-modernization-blockers b/scripts/check-modernization-blockers index 49935e6677..c143c9680c 100755 --- a/scripts/check-modernization-blockers +++ b/scripts/check-modernization-blockers @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f: print(json.load(f).get('version', '')) PY ) -if [[ "$version" != "1.6.175" ]]; then - echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.175" +if [[ "$version" != "1.6.176" ]]; then + echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.176" failures=$((failures + 1)) fi @@ -59,7 +59,7 @@ require_no_pattern 'class="(loader|orbit|loadfield|grass|sun|moon)"' \ 'legacy product loading artwork returned to the application transition' \ app/application/template.hbs -require_no_pattern '\(action[[:space:]]+\(mut' \ +require_no_pattern '(\{\{|\()action[[:space:]]+\(mut' \ 'removed action-mut composition returned instead of the supported fn-mut contract' \ app diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace index f767b59139..5c9b782f89 100755 --- a/scripts/check-ui-console-workspace +++ b/scripts/check-ui-console-workspace @@ -141,4 +141,4 @@ if [[ -n ${PASTURESTACK_PRIVATE_MARKER:-} ]] && grep -RInF -- "$PASTURESTACK_PRI fi printf 'UI_CONSOLE_WORKSPACE_OK version=%s persistence=%s cross_tab=%s\n' \ - 1.6.175 browser-session broker-broadcast + 1.6.176 browser-session broker-broadcast diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies index a72e8b835c..6eafe81910 100755 --- a/scripts/check-ui-critical-high-dependencies +++ b/scripts/check-ui-critical-high-dependencies @@ -70,7 +70,7 @@ if lock_bytes != baseline_bytes: lock = json.loads(lock_bytes) packages = lock.get("packages", {}) root = packages.get("", {}) -if package.get("version") != "1.6.175": +if package.get("version") != "1.6.176": fail(f"unexpected Web Console version: {package.get('version')}") if root.get("version") != package.get("version"): fail(f"lock root version differs: {root.get('version')}") diff --git a/tests/integration/components/view-edit-project-permissions-test.js b/tests/integration/components/view-edit-project-permissions-test.js index bcca300866..e1d4ac0839 100644 --- a/tests/integration/components/view-edit-project-permissions-test.js +++ b/tests/integration/components/view-edit-project-permissions-test.js @@ -1,9 +1,9 @@ import { A } from '@ember/array'; import Component from '@ember/component'; -import EmberObject from '@ember/object'; +import EmberObject, { get } from '@ember/object'; import Service from '@ember/service'; import { precompileTemplate } from '@ember/template-compilation'; -import { click, find, findAll, render, settled, setupContext, setupRenderingContext, teardownContext } from '@ember/test-helpers'; +import { click, find, findAll, render, select, settled, setupContext, setupRenderingContext, teardownContext } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { initialize as initializePodLayouts } from 'ui/initializers/pod-component-layouts'; @@ -67,6 +67,63 @@ module('Integration | Component | view edit project permissions', function(hooks this.testRoot.remove(); }); + for (let memberType of ['plain object', 'EmberObject']) { + test(`native role select updates the selected ${memberType} without metadata or network writes`, async function(assert) { + let attributes = { + displayType: 'User', externalId: 'member-2', externalIdType: 'oidc_user', + name: 'Alpha Member', role: 'member', + }; + let member = memberType === 'plain object' ? {...attributes} : EmberObject.create(attributes); + let owner = {displayType: 'User', externalId: 'owner-1', externalIdType: 'oidc_user', name: 'Zulu Owner', role: 'owner'}; + let writes = {project: 0, members: [], network: 0, refresh: 0, done: 0}; + this.project.setProperties({ + actionLinks: {setmembers: '/projects/1a21?action=setmembers'}, + projectMembers: A([owner, member]), + validationErrors() { return A([]); }, + save() { writes.project++; return Promise.resolve(this); }, + doAction(action, payload) { + assert.strictEqual(action, 'setmembers', 'the native save submits only memberships'); + writes.members.push(payload); + return Promise.resolve(); + }, + }); + this.network.set('save', () => { writes.network++; return Promise.resolve(); }); + this.owner.lookup('service:projects').set('refreshAll', () => { writes.refresh++; }); + this.done = () => { writes.done++; }; + this.userStore = {getById() { return EmberObject.create({resourceFields: {role: {options: ['owner', 'member', 'readonly']}}}); }}; + + await render(precompileTemplate(`{{view-edit-project + project=this.project originalProject=this.originalProject network=this.network + policyManager=this.policyManager userStore=this.userStore showEdit=true editing=true done=this.done + }}`)); + + let rows = findAll('table.grid tbody tr'); + let selectedRow = rows.find((row) => row.querySelector('[data-test-member-name]').textContent.trim() === attributes.name); + let roleSelect = selectedRow.querySelector('select'); + assert.strictEqual(rows[0], selectedRow, 'sorting selects the member rather than its original array index'); + assert.strictEqual(roleSelect.value, 'member'); + await select(roleSelect, 'readonly'); + assert.strictEqual(get(member, 'role'), 'readonly', 'input/change writes the real selected model'); + assert.strictEqual(this.project.get('projectMembers')[1], member, 'the arranged row retains its original model reference'); + assert.deepEqual({externalId: get(member, 'externalId'), externalIdType: get(member, 'externalIdType'), name: get(member, 'name')}, + {externalId: attributes.externalId, externalIdType: attributes.externalIdType, name: attributes.name}, 'identity and label are unchanged'); + assert.strictEqual(owner.role, 'owner', 'the owner row is not changed'); + assert.strictEqual(this.project.get('description'), 'Description', 'role selection does not change metadata'); + assert.true(find('input[type="text"]').disabled, 'members-only capability does not unlock metadata'); + assert.strictEqual(findAll('.radio input').length, 0, 'members-only capability does not unlock network policy'); + assert.deepEqual(writes.members, [], 'selecting alone sends no save'); + + await click('.footer-actions .btn-primary'); + assert.deepEqual(writes.members, [{members: [ + {type: 'projectMember', externalId: owner.externalId, externalIdType: owner.externalIdType, role: 'owner'}, + {type: 'projectMember', externalId: attributes.externalId, externalIdType: attributes.externalIdType, role: 'readonly'}, + ]}], 'native save sends the selected role with exact original identities'); + assert.deepEqual({project: writes.project, network: writes.network, refresh: writes.refresh, done: writes.done}, + {project: 0, network: 0, refresh: 1, done: 1}, 'save capability and finalizer remain scoped to memberships'); + assert.strictEqual(owner.role, 'owner'); + }); + } + test('new environment cards render real native names and select the same ID after a rename', async function(assert) { this.app = EmberObject.create({baseAssets: '/'}); let zulu = ProjectTemplate.create({id: '1pt-zulu', name: 'Zulu environment', stacks: A([]), app: this.app}); diff --git a/tests/integration/helpers/legacy-template-action-test.js b/tests/integration/helpers/legacy-template-action-test.js index 86d30b64ad..b30c8079c9 100644 --- a/tests/integration/helpers/legacy-template-action-test.js +++ b/tests/integration/helpers/legacy-template-action-test.js @@ -1,6 +1,9 @@ import { module, test } from 'qunit'; -import { fillIn, render, setupContext, setupRenderingContext, teardownContext } from '@ember/test-helpers'; +import { fillIn, find, render, select, setupContext, setupRenderingContext, teardownContext } from '@ember/test-helpers'; import { precompileTemplate } from '@ember/template-compilation'; +import Service from '@ember/service'; + +import { initialize as initializePodLayouts } from 'ui/initializers/pod-component-layouts'; import resolver from '../../helpers/resolver'; @@ -10,6 +13,8 @@ module('Integration | Helper | legacy template action', function(hooks) { this.testRoot.id = 'ember-testing'; document.body.appendChild(this.testRoot); await setupContext(this, { resolver }); + initializePodLayouts(); + this.owner.register('service:intl', Service.extend({t(key) { return key; }})); await setupRenderingContext(this); }); @@ -27,4 +32,20 @@ module('Integration | Helper | legacy template action', function(hooks) { await fillIn('input', 'sleep 3600'); assert.deepEqual(this.command, ['sleep', '3600'], 'the rendered command field writes the bound array'); }); + + test('native enum select preserves the mut setter and changes only its bound model field', async function(assert) { + this.enumModel = {id: 'unchanged-model', value: 'first', metadata: {stable: true}}; + this.enumField = {options: ['first', 'second']}; + let metadata = this.enumModel.metadata; + await render(precompileTemplate('{{schema/input-enum field=this.enumField value=this.enumModel.value}}')); + + assert.strictEqual(find('select').value, 'first'); + await select('select', 'second'); + assert.strictEqual(this.enumModel.value, 'second', 'real input/change invokes the native fn-mut setter'); + assert.strictEqual(find('select').value, 'second', 'the selected option agrees with the model'); + assert.strictEqual(this.enumModel.id, 'unchanged-model'); + assert.strictEqual(this.enumModel.metadata, metadata, 'unknown metadata is not replaced'); + assert.deepEqual(metadata, {stable: true}); + assert.deepEqual(this.enumField.options, ['first', 'second'], 'schema choices remain unchanged'); + }); });