location and ou stop pushing a value across a group (ADR 0038 decision 13) - #36
Merged
Conversation
…ecision 13) Both plugins shipped a second hook file -- AddLocationGroup, AddOUGroup -- whose only job was to set one value on every member of a group. Neither was salvageable, for two separate reasons. It was a COPY, not a grant. The hook read the membership at the instant the button was pressed and wrote a row per member, so a host added to the group afterward got nothing and a host removed kept what it had. That is exactly the model ADR 0038 removes from the core group page. And it ALWAYS CLOBBERED. Every save ran a deletemass over every member before inserting, so "leave this host alone" was not a state the form could express. Saving the tab to set one host's location wiped it from every other member of the group. These do not become grants. A location and an OU are single-valued per host -- the write path has always been delete-then-insert-one. A grant is a SET that several groups union into, and unioning two locations means nothing. Single-valued settings go to the host list's mass edit, which is where ADR 0038 sent the core group page's imperative fields, and decision 13 added HOST_MASSEDIT_* precisely so a plugin could follow. So each plugin now contributes one field through HOST_MASSEDIT_FIELDS and applies it through HOST_MASSEDIT_APPLY, and the group hook, its JS, and the 'group' entry in its PAGE_JS_FILES mapping are deleted. Core draws the three-state action control, which is what makes "leave alone" expressible at all. Two details worth naming. The value control renders EMPTY: there is no honest value to pre-fill from a selection whose hosts disagree, so what they hold is stated in the hint instead. And the hint goes through SharedHostValues::forHostRows() rather than a hand-rolled query, because a host with no location has no row at all -- counting rows would call three hosts out of five "in agreement". forHostRows() compares the row count to the selection size for that reason. Its answer is in the column's own terms, so a uniform selection comes back as an id; the name is looked up before rendering, and only when there is one name to show. A 'set' naming a record that does not exist THROWS rather than falling through to the delete. Treating it as a clear would strip the value off every selected host -- the old clobber arriving by a new route. tests/group-push-became-mass-edit.test.php drives the apply path for real. That needed FOG\Base\Hook in the stubs, which had never been there: every hook extends it, so no hook could be loaded at all and everything a hook decided had to be pinned by reading its source. That is the right default for a hook that echoes a form and the wrong one for a hook that writes, where the branch taken decides whether rows survive. Proven by mutation: dropping the 'leave' guard, turning an invalid id into a clear, registering only the field event, reporting the raw id in the hint, dropping the empty-selection guard, and restoring the old group hook each turn it red. The last of those trips a generic check as well as the filename one, so a newly written group-push hook fails it too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166dqQEjAs9fhqUw5zCjvxM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Both plugins shipped a second hook file —
AddLocationGroup,AddOUGroup— whose only job was to set one value on every member of a group. Neither was salvageable, for two separate reasons.It was a copy, not a grant. The hook read the membership at the instant the button was pressed and wrote a row per member, so a host added to the group afterward got nothing and a host removed kept what it had. Exactly the model ADR 0038 removes from the core group page.
And it always clobbered. Every save ran a
deletemassover every member before inserting, so "leave this host alone" was not a state the form could express. Saving the tab to set one host's location wiped it from every other member of the group.Why they don't become grants
A location and an OU are single-valued per host — the write path has always been delete-then-insert-one. A grant is a set that several groups union into, and unioning two locations means nothing.
Single-valued settings go to the host list's mass edit, which is where ADR 0038 sent the core group page's imperative fields, and decision 13 added
HOST_MASSEDIT_*precisely so a plugin could follow. These two are the first plugins to use that seam.What changed
Each plugin contributes one field through
HOST_MASSEDIT_FIELDSand applies it throughHOST_MASSEDIT_APPLY. The group hook, its JS, and the'group'entry in itsPAGE_JS_FILESmapping are deleted. Net −2 files per plugin. Core draws the three-state action control, which is what makes "leave alone" expressible at all.Two details worth naming:
SharedHostValues::forHostRows(), not a hand-rolled query — a host with no location has no row at all, so counting rows would call three hosts out of five "in agreement".forHostRows()compares the row count to the selection size for that reason. Its answer is in the column's own terms, so a uniform selection comes back as an id; the name is looked up before rendering, and only when there is one name to show.A
setnaming a record that does not exist throws rather than falling through to the delete. Treating it as a clear would strip the value off every selected host — the old clobber arriving by a new route.Proof
tests/group-push-became-mass-edit.test.phpdrives the apply path for real. That neededFOG\Base\Hookin the stubs, which had never been there: every hook extends it, so no hook could be loaded at all and everything a hook decided had to be pinned by reading its source. Fine for a hook that echoes a form; wrong for one that writes, where the branch taken decides whether rows survive.Proven by mutation, each turning it red:
leaveguard (the old clobber)The last trips a generic check as well as the filename one — no hook may
deletemassover a group's membership — so a newly written group-push hook fails it too.Full suite 16/16.
Follow-on: this unblocks removing the deprecated imperative cards from the core group page.
🤖 Generated with Claude Code
https://claude.ai/code/session_0166dqQEjAs9fhqUw5zCjvxM