Skip to content

Saving a group or event changes only the fields sent - #930

Open
edwh wants to merge 6 commits into
developfrom
group-partial-updates
Open

edwh wants to merge 6 commits into
developfrom
group-partial-updates

Conversation

@edwh

@edwh edwh commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Saving a group or an event through the API (PATCH /api/v2/groups/{id}, PATCH /api/v2/events/{id}) changes only the fields the request sends. Both endpoints follow the same rule.

Groups

  • Fields left out. Before: every field was written, so a field missing from the request was blanked. Now: fields left out are kept; sending just a phone number changes only the phone number. Fields sent empty or null are cleared, so a client can remove a website, phone number or archive date.
  • Location. Before: a request without a location blanked the location, map position and country. Now: sending a location sets the map position and country from geocoding it; without one, all three are left alone.
  • Timezone. Before: a request without a timezone, or with an empty one, set future events' timezone to empty. Now: future events move only when the request sends a different, non-empty timezone. Clearing the group's timezone leaves events' times where they are.
  • Area and archive date. Before: an administrator or network coordinator saving without them blanked them, which unarchived the group. Now: they change only when sent, and only by an administrator or the group's network coordinator. Hosts can change the postcode.
  • Archive action. Before: it sent the whole group back, saving network data as the string "[object Object]". Now: it sends only the group id and archive date, leaving the rest of the group untouched. An empty archive date unarchives.
  • Approval. Before: approving without resending the location notified no nearby volunteers. Now: they are notified from the group's saved position.

Events

  • Fields left out. Before: start, end, title and description had to be sent every time, and anything else left out (link, location, timezone, online) was blanked. Now: every field is optional and fields left out are kept; fields sent empty or null are cleared. Start, end, title and description are rejected only if sent empty.
  • Times. Sending only the start or only the end keeps the other and recalculates the event's length.
  • Location. Sending a location sets the map position from geocoding it; without one, both are left alone.
  • Edit form. The Vue form sends every field, including blank ones and "online" turned off, so clearing the link or switching an event back from online works as it did. The Nuxt form already sends every field.
  • WordPress. The event push to WordPress reads the saved event, so it has every field even after a partial update.

Compatibility

  • The Vue and Nuxt edit forms send every field the user can change, so editing a group or event through them behaves the same.
  • Zapier and Repair Together only use the v1 read routes, and no artisan command, job or listener calls these endpoints.
  • An external API client that relied on omitted fields being blanked would now keep them; that was never documented.

Test plan

  • PHPUnit, groups: a host sending only a phone number leaves every other field, and future events' timezone, unchanged.
  • PHPUnit, groups: sending a field empty or null clears it; an empty archive date unarchives.
  • PHPUnit, groups: archiving keeps the name and network data, and is audited as a change to the archive date only.
  • PHPUnit, groups: a new location brings its map position and country; a new timezone moves future events; clearing the timezone leaves them alone.
  • PHPUnit, groups: a host can't archive or change the area; approving without the location notifies nearby volunteers.
  • PHPUnit, events: sending only the link changes nothing else; sending only the start keeps the end and recalculates the length; an empty link and online off are cleared; a new location brings its map position; an empty title is rejected.
  • Jest: the Archive action sends only the group id and archive date; the events store sends blanks and booleans.
  • CI

- updateGroupv2 updates only the fields present in the request (location
  brings its geocoded lat/lng/country with it). A partial update previously
  blanked everything else: e.g. sending just phone wiped the name, location,
  coordinates, description, website, timezone, email and network_data.
- A future-event timezone change only happens when timezone is sent;
  omitting it no longer counts as a change to null.
- The Archive group action sends only {id, archived_at}. It sent the whole
  store group back, which saved network_data as "[object Object]".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@edwh edwh mentioned this pull request Oct 1, 2026
2 of 3 tasks
@ngm ngm assigned ngm and edwh and unassigned ngm Oct 8, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@edwh

edwh commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator Author

Assessment: API update consistency and backwards compatibility

Reviewed at head c5a1882. CI is green. Recommendation: merge, with two small follow-ups noted below.

Clients and compatibility

Client Sends Effect under the PR
Vue edit form (GroupAddEdit.vue:505-518 via store/groups.js:265-290) Every field on each save. The store drops null/undefined keys (groups.js:271-273), so an empty text box arrives as "". Unchanged. "" is a key that is present, so it is saved as before.
Vue Archive action (GroupActions.vue:128-135) Before: the whole store group. After: {id, archived_at}. Fixed, see Archive.
Nuxt client, origin/nuxt-client GroupForm.vue:247-277 and stores/groups.js:488 name, website, email, phone, description, location, timezone, postcode always. Blanks go as JSON null. network_data and area/tags/networks only when editable. It never sends archived_at. Works, and needs the PR. The Nuxt form already assumes PATCH writes only the fields sent (comment at GroupForm.vue:254-259). Under the old code every Nuxt save would have nulled archived_at for admins.
Nuxt archive (stores/groups.js:396-406) DELETE /api/v2/groups/{id} Not touched by the PR. There is no such route on develop (routes/api.php:74-85), so this belongs to the Nuxt branch.
Zapier, Repair Together Only the v1 read routes /groups/changes and /groups/network (routes/api.php:42-47) None call PATCH.
Artisan commands, jobs, listeners None call updateGroupv2. Group::update is used directly in PopulateUniqueCodeToEventsAndGroups and CreateWordpressPostForGroup:94. Not affected.
Third parties The endpoint is documented in the OpenAPI annotations, with no required body fields in the update annotation. An external client that relied on omitted fields being blanked would now keep them. That reliance is unlikely and was never documented.

Field semantics (omit / empty / null)

The app runs ConvertEmptyStringsToNull (app/Http/Kernel.php:21), so "" and null are the same thing by the time the controller sees them. The PR gates on $request->has(key), which is true for a present key even when its value is null. So "present but empty" clears the field, and only an absent key preserves it. A client can still clear any field, as long as it sends the key.

Field Omitted: old / PR "" or null: old / PR
name, website, phone, email, description (free_text), network_data Blanked / kept Cleared / cleared
location, latitude, longitude, country All four blanked / kept All four cleared / all four cleared (location present with an empty value)
timezone Blanked, and counted as a change so future events were also set to null / kept, events untouched Cleared, and future events set to null / same (see follow-up 2)
postcode Kept since #917 / same Cleared / cleared
area, archived_at (admin or the group's coordinator only) Blanked / kept Cleared (archived_at null means unarchive) / same
networks, tags, moderate, image Already guarded ($request->networks, ->tags, input('moderate'), $_FILES) / same Same as before

The only way to lose the ability to clear a field is to omit the key. Neither the Vue form nor the Nuxt form does that for a field the user can empty.

Note that location carries latitude, longitude and country_code with it (GroupController.php around the $request->has('location') block). Sending the unchanged location string skips geocoding (validateGroupParams).

Archive

  • Before, the action posted the whole store group. The store appends every non-null value to FormData (groups.js:271-273), so the object-valued network_data became the string [object Object]. The PR cites this correctly.
  • After, the payload is {id, archived_at} (GroupActions.vue:131-134). The Jest test pins the exact keys.
  • The server now writes only archived_at. The audit log therefore records one changed attribute instead of a rewrite of the whole group (the Group model is auditable, app/Group.php:20).
  • Unarchiving: there is no unarchive button in the Vue client. Over the API, archived_at: "" or null clears it, because the key is present and the rule is nullable|date (validateGroupParams). That path is not tested.

Consistency with other endpoints

  • createGroupv2 takes the same validateGroupParams and writes every field, which is correct for a create.
  • updateEventv2 (API\EventController.php:652-682) is still a full replace. It builds the whole $update array and writes it. Events and groups now differ: PATCH on a group is partial, PATCH on an event is not. Not changed here, but worth a ticket if the "PATCH means partial" convention is meant to be general.
  • The volunteer PATCH (patchVolunteerForGroupv2) is already per-field.

Test gaps

Covered: a host sending only phone, with every other field and the future-event timezone unchanged. An admin sending only archived_at, which keeps name and network_data. Jest on the archive payload.

Not covered:

  1. Clearing a field with "" and with JSON null (the main compatibility promise).
  2. Unarchiving with archived_at empty.
  3. Sending a changed location in a partial update brings new coordinates and country with it.
  4. Sending a changed timezone still moves future events.
  5. A host sending area or archived_at is ignored.
  6. The Nuxt payload shape (nulls for blanks, no archived_at).
  7. The audit entry for an archive holds only archived_at.
  8. Approving with a partial payload (see follow-up 1).

Why the earlier attempt was reverted

Commit de5e718 was reverted by 759b85d with no stated reason. The context is that #917 was being cut down to the host postcode change, and its body now says these changes "are now in #930". No bug was found in the original. Its code differs from this PR only in how postcode is handled, and postcode now lives in #917 behind $request->has('postcode'). So the PR addresses the reason, which was scope, not a defect.

Recommendation

Merge. It fixes the corruption from the Archive action, it is required by the Nuxt client, and it keeps the ability to clear any field by sending it. Two follow-ups, neither a blocker:

  1. Approve without location. In the moderate=approve branch, "notify nearby users" uses the request's $latitude/$longitude, which are null when location is not sent. A partial approve would skip those notifications even though the group has coordinates. Using $group->latitude/$group->longitude after the update would fix it. Both existing clients send location, so nobody is hit today.
  2. Clearing the timezone. Sending an empty timezone sets future events' timezone to null. This is old behaviour, and the Nuxt form can now trigger it (timezone: form.timezone || null). Guard with !empty($timezone) on the event update.

Also worth adding the tests listed above, especially 1, 2 and 8.

edwh and others added 2 commits October 11, 2026 22:19
# Conflicts:
#	resources/js/components/GroupActions.test.js

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ges, host limits and approval

Approving a group now notifies nearby volunteers from the group's saved
position, so an approval that doesn't resend the location still notifies them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ngm

ngm commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

@edwh Two things before merging:

  • There's a merge conflict now, can you resolve?
  • And, there is a recommendation to add the tests that are currently missing. Worth adding them now? Feels like we'd be unlikely to come back to them?

… timezone leaves events alone

- PATCH /api/v2/events/{id} changes only the fields the request sends, like groups. Start, end, title and
  description are required only when sent. Sending one end of the event recalculates the hours from the
  saved other end. A location brings its map position.
- The Vue events store sends blanks and booleans so the edit form can still clear the link or turn off online.
- The WordPress event push reads the saved event, not the update payload.
- An empty group timezone no longer sets future events' timezone to empty.
- Tests carry @story refs; manifest regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@edwh edwh changed the title Saving a group changes only the fields sent Saving a group or event changes only the fields sent Oct 14, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@edwh

edwh commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

@ngm Ready to merge, please. CircleCI and SonarCloud are green.

  • Merge conflict: resolved. develop is merged in.
  • Tests: the missing ones were added on 12 Oct: clearing fields, unarchiving, location and timezone changes, host limits and approving without a location. There are more now for events and the timezone change below.
  • Consistent API: saving an event (PATCH /api/v2/events/{id}) now follows the same rule as groups. Fields left out are kept, and fields sent empty are cleared. Before, it needed start, end, title and description every time and blanked the rest. The edit form sends every field, so editing an event on the site behaves as it did, including clearing the link or switching an event back from online.
  • Empty timezone: clearing a group's timezone now leaves its future events' times alone. Before, it blanked them.

The description above lists what each endpoint now does.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants