Files
twenty/packages
Félix Malfait 33970dd45d Align campaign view column positions with the standard layout (#23496)
Follow-up to #23493 (merged). Now based on `main`.

## Problem

`AddMessageCampaignNameFieldCommand` places the new `name` column below
the lowest existing position when it is the label identifier (`min -
1`). That satisfies `isViewFieldInLowestPosition` once, but does not
hold: `viewField.position` is compared by the standard-application sync
(`position: { toCompare: true }`), so the stored position is pulled back
to the standard one and re-fails
`validateLabelIdentifierFieldMetadataIdFlatViewField`, throwing
`WorkspaceMigrationBuilderException` on every sync. That sync runs
during `WorkspaceManagerService.init`, which is what `ActivateWorkspace`
calls.

cubic flagged this on the original PR:
[discussion_r3653452416](https://github.com/twentyhq/twenty/pull/23188#discussion_r3653452416).

## Confirmed against prod

Queried the workspace failing in Sentry
([TWENTY-SERVER-JJ2](https://twenty-v7.sentry.io/issues/7639768673/)):
the `allMessageCampaigns` view has no `name` column at all, `subject` at
position `0` (still the label identifier), and every column at the old
layout. The activation sync tries to create `name` at standard position
`0` while repointing the label identifier in the same batch, ties with
`subject` at `0`, and throws. Exactly the mechanism this PR fixes.

## Change

A `2-25` workspace command
(`upgrade:2-25:align-message-campaign-view-field-positions`, timestamp
`1785332560000`) that aligns the `allMessageCampaigns` columns to the
standard layout, so the sync has nothing left to change:

- Standard columns take their standard positions (`subject 0→1`, `status
1→2`, …), freeing slot `0` for `name`.
- Columns the standard application does not know about keep their
relative order and move above the standard ones.
- Updates run in two migration passes: everything else first, then the
lowest-target column alone. The migration builder validates updates one
at a time against optimistic maps it mutates as it goes, so a single
batch is order-dependent (caught by Greptile below); two passes keep
every intermediate state valid.

Placed in `2-25/` rather than `2-26/` so it can ship in a 2.25.x patch —
the failures are on `v2.25.0` instances. This is why
`server-previous-version-upgrade-mutation-guard` is red: it needs the
`ci:allow-previous-version-upgrade-mutation` label (same as #23493).

The position arithmetic and the pass-splitting are pure utils with 12
tests.

## Traced against the confirmed prod state

Pass 1 moves `status`…`createdAt` up while `subject` (label identifier)
stays lowest at `0`; pass 2 moves `subject` to `1`, still strictly
lowest. The subsequent activation sync then creates `name` at `0` below
`subject` at `1` and repoints the label identifier. Converges.

Run order matters only relative to the sibling command from #23493:
removal (`…550000`) sorts before alignment (`…560000`), which is
correct.
2026-07-29 16:16:52 +02:00
..
2026-07-29 16:06:23 +02:00