fix(calendar): apply an eForm change to every uncompleted occurrence - #1114
Merged
renemadsen merged 2 commits intoAug 19, 2026
Merged
Conversation
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
There was a problem hiding this comment.
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
RepairEformForOpenOccurrencesAsyncto retract/redeploy open occurrences on eForm change (per-site, idempotent, with safety guards and time-bounded cloud delete). - Backend: ensure
Planning.RelatedEFormIdstays in sync across all relevant wizard update branches; add calendar scope="this" widening viaApplyEformChangeToSeries. - Frontend/tests: type
eformIdin 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
deleted the
fix/calendar-eform-change-propagates-to-open-occurrences
branch
August 19, 2026 14:13
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.
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 resolvesCompliance.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.RepairEformForOpenOccurrencesAsyncretracts and redeploys a planning's open cases when its eForm changes, per site:Compliancerows are re-pointed in place, soCompliance.Idstays stable (the calendar UI holdscomplianceId)PlanningCase/PlanningCaseSitecarry the new eForm idThe calendar deploy path creates one
PlanningCaseper site whileComplianceis 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 asmainElement.EndDate, whichCaseCreateLocalOnlyvalidates and then discards (SqlController.CaseCreatepersists no date), so a replacement created with a nominal date is identical in every persisted column.The eForm stays a series-level property.
UpdateTaskThisOccurrencepreviously 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.RelatedEFormIdis written in all fouroldStatusbranches (the reactivation branch left it stale, which then wrote a wrongCompliance.MicrotingSdkeFormId)Compliance.MicrotingSdkeFormIdcomes from the id the case was actually created witheformIdis typed on the calendar task request models instead of riding on ananypayloadGuards added after two review gates
PlanningCase(otherwise reactivating + changing the eForm resurrected cancelled occurrences on the worker's device)StreamEventChangespollCaseDeleteis bounded by a 30s timeout, so the SDK'sThread.Sleep(i * 5000)retry loop cannot hold the request thread for hoursTests
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 realeFormCore.Coreon 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'stemplateIdis 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, anddevgetchanges.shdoes not carryPage objects/.Known limitation, not fixed here
Multi-site calendar events accumulate duplicate deployments: the site-scoped idempotence guard in
EnsureDeployedAsynclooks for aCompliancerow 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