From 67e701b538fb51c93f45437e83e2d3801af63841 Mon Sep 17 00:00:00 2001 From: "benjamin.baker" Date: Tue, 8 Sep 2026 17:16:34 -0400 Subject: [PATCH] Let the policy list edit the rows the API accepts The shipped Configuration Policies rendered with working-looking Edit and Delete buttons that did nothing. The API had already been widened to accept shipped rows, but the buttons still carried :disabled on source !== local, so the write was never sent and the change looked like it had never rolled. Gate both buttons on the set the API actually accepts. The shipped badge stays: it is provenance, not a lock, and it has to survive an edit so a site can still tell a product-supplied row from one of its own. Business Policies keep the local-only rule. Their API guard is unchanged and no shipped Site Policy exists now, so the UI already matches. Cover it with a spec, because the failure mode is a disabled attribute silently disagreeing with a server guard - nothing errors, the button just does nothing. --- .../views/ConfigurationPoliciesList.vue | 16 +++++++++-- .../views/configurationPolicyEditable.spec.js | 27 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 plugins/computers/frontend/views/configurationPolicyEditable.spec.js diff --git a/plugins/computers/frontend/views/ConfigurationPoliciesList.vue b/plugins/computers/frontend/views/ConfigurationPoliciesList.vue index 45eb98ae..b5badc89 100644 --- a/plugins/computers/frontend/views/ConfigurationPoliciesList.vue +++ b/plugins/computers/frontend/views/ConfigurationPoliciesList.vue @@ -42,8 +42,8 @@ - - + + @@ -287,6 +287,18 @@ async function save() { } } +// Mirrors the API guard in plugins/computers/api/policy_routes.py: local, +// autoapp and shipped rows are all editable. A shipped row is a starting point +// the product supplies, not a fixed opinion, and it keeps source='shipped' so +// provenance survives the edit. Only a genuinely external managed owner is +// read-only. Disabling on source==='local' alone stranded shipped rows behind +// dead buttons while the API was accepting the write. +const EDITABLE_SOURCES = ['local', 'autoapp', 'shipped'] + +function iseditable(p) { + return EDITABLE_SOURCES.includes(p.source) +} + async function remove(p) { if (!confirm(`Delete configuration policy "${p.name}"?`)) return try { diff --git a/plugins/computers/frontend/views/configurationPolicyEditable.spec.js b/plugins/computers/frontend/views/configurationPolicyEditable.spec.js new file mode 100644 index 00000000..9dec5bdc --- /dev/null +++ b/plugins/computers/frontend/views/configurationPolicyEditable.spec.js @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'fs' +import { fileURLToPath } from 'url' +import { dirname, join } from 'path' + +const here = dirname(fileURLToPath(import.meta.url)) +const view = readFileSync(join(here, 'ConfigurationPoliciesList.vue'), 'utf8') + +// The API decides what may be edited; the buttons only reflect that decision. +// These drifted once: policy_routes.py was widened to accept shipped rows while +// the buttons still disabled on source !== 'local', so every shipped policy sat +// behind a dead Edit button and the change looked like it had never rolled. +// If the API guard changes, EDITABLE_SOURCES here changes with it. +describe('the configuration policy buttons follow the API guard', () => { + it('treats shipped as editable, not read-only', () => { + expect(view).toContain("EDITABLE_SOURCES = ['local', 'autoapp', 'shipped']") + }) + + it('does not gate the buttons on local alone', () => { + expect(view).not.toContain(":disabled=\"p.source !== 'local'\"") + }) + + it('gates edit and delete on the same rule', () => { + const disabled = view.match(/:disabled="!iseditable\(p\)"/g) || [] + expect(disabled).toHaveLength(2) + }) +})