Skip to content

fix(calendar): apply an eForm change to every uncompleted occurrence - #1114

Merged
renemadsen merged 2 commits into
stablefrom
fix/calendar-eform-change-propagates-to-open-occurrences
Aug 19, 2026
Merged

fix(calendar): apply an eForm change to every uncompleted occurrence#1114
renemadsen merged 2 commits into
stablefrom
fix/calendar-eform-change-propagates-to-open-occurrences

Conversation

@renemadsen

Copy link
Copy Markdown
Member

The bug

Changing the selected eForm on an existing calendar event updated what the calendar displayed, but completing the event still opened the eForm chosen at creation.

Root cause

The calendar renders AreaRule.EformId, which the edit path always rewrites. Completion instead resolves Compliance.Id → Compliance.MicrotingSdkCaseId → Cases.CheckListId, and that checklist is frozen when the case is deployed (ReadeForm + CaseCreateLocalOnly).

No code compared the old and new eForm id, nothing retracted or redeployed on a change, and the deploy idempotence guard keys on (PlanningId, Deadline[, site]) — never on the eForm. So any already-deployed occurrence kept the original form, while one not yet deployed materialised on demand and got the new one.

The fix

EventDeployService.RepairEformForOpenOccurrencesAsync retracts and redeploys a planning's open cases when its eForm changes, per site:

  • completed cases and cases already on the new eForm are skipped
  • Compliance rows are re-pointed in place, so Compliance.Id stays stable (the calendar UI holds complianceId)
  • PlanningCase / PlanningCaseSite carry the new eForm id

The calendar deploy path creates one PlanningCase per site while Compliance is unique on (PlanningId, Deadline), so sites 2..n are reachable from no Compliance row. A second sweep covers them. No schema change is needed: the rotation date reaches the SDK only as mainElement.EndDate, which CaseCreateLocalOnly validates and then discards (SqlController.CaseCreate persists no date), so a replacement created with a nominal date is identical in every persisted column.

The eForm stays a series-level property. UpdateTaskThisOccurrence previously dropped an eForm change silently; it now applies it to the series, and the edit dialog confirms first when the scope is narrower than "all".

Also fixed on the way

  • Planning.RelatedEFormId is written in all four oldStatus branches (the reactivation branch left it stale, which then wrote a wrong Compliance.MicrotingSdkeFormId)
  • Compliance.MicrotingSdkeFormId comes from the id the case was actually created with
  • eformId is typed on the calendar task request models instead of riding on an any payload

Guards added after two review gates

  • the repair never runs for a task that ends inactive, and never revives rows under a Removed/Retracted PlanningCase (otherwise reactivating + changing the eForm resurrected cancelled occurrences on the worker's device)
  • sites that are no longer live assignees are retracted rather than redeployed — the cross-worker-leak guard now covers the repair path too
  • the new eForm id is validated before anything destructive, so an unreadable id cannot strip every occurrence of its case
  • each swap takes the same per-(planning, site) deploy lock the deploy path uses, closing the retract→create window against the 5s StreamEventChanges poll
  • a failed replacement releases the Compliance row so the stuck-row branch can redeploy it
  • the cloud CaseDelete is bounded by a 30s timeout, so the SDK's Thread.Sleep(i * 5000) retry loop cannot hold the request thread for hours

Tests

15 integration tests — deployed/open, completed, not-yet-deployed, multi-site (both the wizard shape and the calendar shape), overdue, orphan-only, scope this, no-change idempotence, reactivation, unassign, deactivate, pre-flight validation, and the failure-compensation path. They run against the real eFormCore.Core on the MariaDB testcontainer and read the DB back, so they genuinely fail against the old behaviour.

1 Playwright spec (t/calendar-eform-change.spec.ts) pinning the actual symptom: create with eForm A, force a deploy, edit to B, then assert the complete modal's templateId is B and that it fetched B's template and case. It also covers the new confirmation dialog (cancel aborts the save; confirm applies to the series).

Depends on

microting/eform-angular-frontend#8014 (merged) — the shared save() page object must dismiss the new dialog, and devgetchanges.sh does not carry Page objects/.

Known limitation, not fixed here

Multi-site calendar events accumulate duplicate deployments: the site-scoped idempotence guard in EnsureDeployedAsync looks for a Compliance row for (PlanningId, Deadline.Date) owned by that site, but the insert for sites 2..n is swallowed by the unique index, so the guard never becomes true for them. Independent of this bug and worth its own issue.

🤖 Generated with Claude Code

https://claude.ai/code/session_015sXLtgzZU8QL9m84GqMkoJ

Changing the selected eForm on an existing calendar event updated what the
calendar displayed, but completing the event still opened the eForm chosen at
creation.

The calendar renders AreaRule.EformId, which the edit path always rewrites.
Completion instead resolves Compliance.Id -> Compliance.MicrotingSdkCaseId ->
Cases.CheckListId, and that checklist is frozen when the case is deployed. No
code compared the old and new eForm id, nothing retracted or redeployed on a
change, and the deploy idempotence guard keys on (PlanningId, Deadline[, site])
— never on the eForm. So any already-deployed occurrence kept the original form.

EventDeployService.RepairEformForOpenOccurrencesAsync now retracts and redeploys
the open cases of a planning when its eForm changes, per site: completed cases
and cases already on the new eForm are skipped, Compliance rows are re-pointed
in place so Compliance.Id stays stable, and PlanningCase/PlanningCaseSite carry
the new eForm id. Because the calendar deploy path creates one PlanningCase per
site while Compliance is unique on (PlanningId, Deadline), a second sweep covers
the sites no Compliance row can reach. No schema change is needed: the rotation
date reaches the SDK only as mainElement.EndDate, which CaseCreateLocalOnly
validates and discards.

The eForm stays a series-level property. UpdateTaskThisOccurrence previously
dropped an eForm change silently; it now applies it to the series, and the edit
dialog asks for confirmation first when the scope is narrower than "all".

Also fixed on the way: Planning.RelatedEFormId is written in all four oldStatus
branches, Compliance.MicrotingSdkeFormId comes from the id the case was actually
created with, and eformId is typed on the calendar task request models instead
of riding on an `any` payload.

Guards added after review: the repair never runs for a task that ends inactive
and never revives rows under a Removed/Retracted PlanningCase; sites that are no
longer live assignees are retracted rather than redeployed; the new eForm id is
validated before anything destructive; each swap takes the same per-(planning,
site) deploy lock the deploy path uses; a failed replacement releases the
Compliance row so it can be redeployed; and the cloud CaseDelete is bounded by a
timeout so the SDK's blocking retry loop cannot hold the request thread.

Tests: 15 integration tests covering deployed/open, completed, not-yet-deployed,
multi-site mixed, overdue, orphan-only, scope "this", no-change idempotence,
reactivation, unassign, deactivate, pre-flight validation and the failure
compensation path; plus a Playwright spec that pins the actual symptom — create
with eForm A, deploy, edit to B, and assert the complete modal fetches B.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sXLtgzZU8QL9m84GqMkoJ
Copilot AI lite review requested due to automatic review settings August 19, 2026 11:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a calendar/task bug where changing an event’s eForm updated what the calendar displayed but did not update the eForm that opens on completion for already-deployed (but not yet completed) occurrences. It introduces a backend repair pass that retracts/redeploys open SDK cases when the series eForm changes, and adds frontend confirmation behavior when an eForm change is made under a narrower edit scope.

Changes:

  • Backend: add RepairEformForOpenOccurrencesAsync to retract/redeploy open occurrences on eForm change (per-site, idempotent, with safety guards and time-bounded cloud delete).
  • Backend: ensure Planning.RelatedEFormId stays in sync across all relevant wizard update branches; add calendar scope="this" widening via ApplyEformChangeToSeries.
  • Frontend/tests: type eformId in calendar task request models, add eForm-scope confirmation modal, and add extensive integration + Playwright regression coverage.

Reviewed changes

Copilot reviewed 41 out of 41 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn/Services/EventDeployService/IEventDeployService.cs Adds contract for repairing open occurrences when eForm changes.
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn/Services/EventDeployService/EventDeployService.cs Implements eForm repair pass, deploy-locking, preflight validation, and bounded cloud delete.
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn/Services/BackendConfigurationTaskWizardService/IBackendConfigurationTaskWizardService.cs Adds API for applying an eForm change to the series.
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn/Services/BackendConfigurationTaskWizardService/BackendConfigurationTaskWizardService.cs Detects eForm changes on update, keeps planning eForm in sync, calls repair pass, and adds ApplyEformChangeToSeries.
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn/Services/BackendConfigurationCalendarService/BackendConfigurationCalendarService.cs Scope="this" edit now widens eForm changes to the series via wizard method.
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn.Integration.Test/TaskWizardEformPropagationTests.cs Adds integration coverage for wizard eForm propagation and guard paths.
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn.Integration.Test/EventDeployServiceEformRepairTests.cs Adds DB-backed integration tests for repair pass behavior and edge cases.
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn.Integration.Test/CalendarUpdateTaskScopeTests.cs Adds tests for scope="this" eForm widening and ensures exception still written.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/calendar/modals/task-create-edit-modal/task-create-edit-modal.component.ts Types save payload, adds board required validation feedback, and adds eForm-change confirmation for non-“all” scopes.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/calendar/modals/index.ts Exports the new eForm-change confirmation modal.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/calendar/modals/eform-change-scope-modal/eform-change-scope-modal.component.ts Adds confirmation dialog component for series-wide eForm impact.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/calendar/modals/eform-change-scope-modal/eform-change-scope-modal.component.html Adds confirmation dialog template + button IDs for tests.
eform-client/src/app/plugins/modules/backend-configuration-pn/modules/calendar/calendar.module.ts Declares/exports the new confirmation modal.
eform-client/src/app/plugins/modules/backend-configuration-pn/models/calendar/calendar-task-request.model.ts Explicitly types eformId on calendar task request models.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/ukUA.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/svSE.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/slSL.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/skSK.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/roRO.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/ptPT.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/ptBR.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/plPL.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/noNO.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/nlNL.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/lvLV.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/ltLT.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/itIT.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/isIS.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/huHU.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/hrHR.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/frFR.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/fiFI.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/etET.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/esES.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/enUS.ts Adds English strings + comments for new modal and board requirement.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/elGR.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/deDE.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/da.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/csCZ.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/src/app/plugins/modules/backend-configuration-pn/i18n/bgBG.ts Adds translations for new confirmation text + “Select a calendar”.
eform-client/playwright/e2e/plugins/backend-configuration-pn/t/calendar-eform-change.spec.ts Adds Playwright regression suite for “completion opens the updated eForm” and new confirm dialog behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 1121 to 1123
const obs = this.isEditMode
? this.calendarService.updateTask(payload, (scope ?? 'this') as RepeatEditScope)
? this.calendarService.updateTask(payload as CalendarTaskUpdateModel, (scope ?? 'this') as RepeatEditScope)
: this.calendarService.createTask(payload);
Comment on lines +17 to +21
// Selected eForm (SDK CheckList id), null when the event has no eForm.
// Declared explicitly so the field is compiler-checked: it used to reach the
// backend only because the modal typed its payload `any`, which made a
// rename/typo silently drop the eForm from the request.
eformId: number | null;
}

var core = await _coreHelper.GetCore();
var sdkDbContext = core.DbContextHelper.GetDbContext();
The preview header bound `selectedTemplate.label`, but `selectedTemplate` is an
EformVisualEditorModel and that model has no `label` — neither in the TS model
nor in the API model it deserializes. The visual-editor endpoint returns the
checklist name as a per-language `translations` list, so the interpolation
resolved to undefined and the label rendered empty for every eForm, in create
and edit mode alike. `strictTemplates` is off in eform-client, so the compiler
never flagged it.

Resolve the name for the current user language instead, reusing the same
translatedName() helper the field-preview rows already use. Its parameter is
widened to the structural shape so it accepts the checklist translations too.

Found by the new calendar-eform-change e2e spec, which asserted the preview
showed the newly selected eForm and got an empty string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sXLtgzZU8QL9m84GqMkoJ
@renemadsen
renemadsen merged commit c824ac1 into stable Aug 19, 2026
57 of 59 checks passed
@renemadsen
renemadsen deleted the fix/calendar-eform-change-propagates-to-open-occurrences branch August 19, 2026 14:13
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