diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f682f6c23..756ffd2811 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,8 @@ and this project adheres to
### Changed
+- Improve version pinning behavior in collaborative editor
+ [#4121](https://github.com/OpenFn/lightning/issues/4121)
- Unify disabled button states across collaborative editor for consistent
styling and behaviour [#4179](https://github.com/OpenFn/lightning/issues/4179)
diff --git a/assets/js/collaborative-editor/components/VersionDropdown.tsx b/assets/js/collaborative-editor/components/VersionDropdown.tsx
index 68d8f63a1b..f7b1101153 100644
--- a/assets/js/collaborative-editor/components/VersionDropdown.tsx
+++ b/assets/js/collaborative-editor/components/VersionDropdown.tsx
@@ -1,5 +1,7 @@
import { useEffect, useRef, useState } from 'react';
+import { useURLState } from '#/react/lib/use-url-state';
+
import { cn } from '../../utils/cn';
import {
useRequestVersions,
@@ -30,11 +32,16 @@ export function VersionDropdown({
const versionsError = useVersionsError();
const requestVersions = useRequestVersions();
+ // Check if version is pinned via URL parameter
+ const { params } = useURLState();
+ const isPinnedVersion = params['v'] !== undefined && params['v'] !== null;
+
// Show placeholder while loading version information
const isLoadingVersion = currentVersion === null || latestVersion === null;
- // Determine if viewing latest version (only when we have both values)
- const isLatestVersion = !isLoadingVersion && currentVersion === latestVersion;
+ // Determine if viewing latest version (only when we have both values AND no pinned version)
+ const isLatestVersion =
+ !isLoadingVersion && currentVersion === latestVersion && !isPinnedVersion;
// Format version display
const currentVersionDisplay = isLoadingVersion
@@ -95,8 +102,8 @@ export function VersionDropdown({
}
}, [versionsError]);
- const handleVersionClick = (version: Version) => {
- if (version.is_latest) {
+ const handleVersionClick = (version: Version | 'latest') => {
+ if (version === 'latest') {
onVersionSelect('latest');
} else {
onVersionSelect(version.lock_version);
@@ -146,44 +153,66 @@ export function VersionDropdown({
No versions available
) : (
- versions.map((version, index) => {
- const isSelected = version.is_latest
- ? isLatestVersion
- : version.lock_version === currentVersion;
-
- // Show "latest" for the latest version, otherwise show version number
- // For the first item (which is latest), show "latest"
- // For subsequent items, show version number even if they have is_latest=true
- const displayText =
- index === 0 && version.is_latest
- ? 'latest'
- : `v${String(version.lock_version).substring(0, 7)}`;
-
- return (
+ <>
+ {/* First, show "latest" option that removes version parameter */}
+ {versions.length > 0 && versions[0].is_latest && (
- );
- })
+ )}
+
+ {/* Then show all versions with version numbers (including latest) */}
+ {versions.map(version => {
+ const isSelected =
+ !isLatestVersion && version.lock_version === currentVersion;
+
+ const displayText = `v${String(version.lock_version).substring(0, 7)}`;
+
+ return (
+
+ );
+ })}
+ >
)}
diff --git a/assets/js/collaborative-editor/components/diagram/CollaborativeWorkflowDiagram.tsx b/assets/js/collaborative-editor/components/diagram/CollaborativeWorkflowDiagram.tsx
index 063f416099..3b5d95f302 100644
--- a/assets/js/collaborative-editor/components/diagram/CollaborativeWorkflowDiagram.tsx
+++ b/assets/js/collaborative-editor/components/diagram/CollaborativeWorkflowDiagram.tsx
@@ -19,14 +19,16 @@ import {
useHistoryLoading,
useRunSteps,
} from '../../hooks/useHistory';
-import { useIsNewWorkflow } from '../../hooks/useSessionContext';
+import {
+ useIsNewWorkflow,
+ useLatestSnapshotLockVersion,
+} from '../../hooks/useSessionContext';
import { useVersionMismatch } from '../../hooks/useVersionMismatch';
import { useNodeSelection } from '../../hooks/useWorkflow';
import { useKeyboardShortcut } from '../../keyboard';
import type { RunSummary } from '../../types/history';
import MiniHistory from './MiniHistory';
-import { VersionMismatchBanner } from './VersionMismatchBanner';
import CollaborativeWorkflowDiagramImpl from './WorkflowDiagram';
interface CollaborativeWorkflowDiagramProps {
@@ -42,6 +44,7 @@ export function CollaborativeWorkflowDiagram({
const isNewWorkflow = useIsNewWorkflow();
const isHistoryChannelConnected = useHistoryChannelConnected();
const { params, updateSearchParams } = useURLState();
+ const latestSnapshotLockVersion = useLatestSnapshotLockVersion();
// Get history data and commands
const history = useHistory();
@@ -85,14 +88,22 @@ export function CollaborativeWorkflowDiagram({
// Find the workorder that contains this run
const workorder = history.find(wo => wo.runs.some(r => r.id === run.id));
+ // Only include version parameter if the run's version differs from latest
+ // This prevents pinning to read-only mode when viewing latest version runs
+ const runVersion = workorder?.version;
+ const shouldPinVersion =
+ runVersion !== null &&
+ runVersion !== undefined &&
+ runVersion !== latestSnapshotLockVersion;
+
// Single atomic update - both version and run in one call
// This prevents race conditions between two separate updateSearchParams calls
updateSearchParams({
- v: workorder ? String(workorder.version) : null,
+ v: shouldPinVersion ? String(runVersion) : null,
run: run.id,
});
},
- [history, updateSearchParams]
+ [history, latestSnapshotLockVersion, updateSearchParams]
);
// Clear URL parameter when deselecting run
@@ -155,14 +166,6 @@ export function CollaborativeWorkflowDiagram({
return (
- {versionMismatch && (
-
- )}
-
)}
diff --git a/assets/js/collaborative-editor/components/diagram/MiniHistory.tsx b/assets/js/collaborative-editor/components/diagram/MiniHistory.tsx
index 21792438b9..f47085a62b 100644
--- a/assets/js/collaborative-editor/components/diagram/MiniHistory.tsx
+++ b/assets/js/collaborative-editor/components/diagram/MiniHistory.tsx
@@ -30,6 +30,7 @@ import { relativeLocale } from '../../../hooks';
import { duration } from '../../../utils/duration';
import truncateUid from '../../../utils/truncateUID';
import { useProject } from '../../hooks/useSessionContext';
+import { useVersionSelect } from '../../hooks/useVersionSelect';
import { useWorkflowState } from '../../hooks/useWorkflow';
import type { RunSummary, WorkOrder } from '../../types/history';
import {
@@ -41,6 +42,8 @@ import { RunBadge } from '../common/RunBadge';
import { ShortcutKeys } from '../ShortcutKeys';
import { Tooltip } from '../Tooltip';
+import { VersionMismatchBanner } from './VersionMismatchBanner';
+
// Extended types with selection state for UI
type RunWithSelection = RunSummary & { selected?: boolean };
type WorkOrderWithSelection = Omit & {
@@ -284,6 +287,11 @@ interface MiniHistoryProps {
// New props for panel variant
variant?: 'floating' | 'panel';
onBack?: () => void;
+ // Version mismatch detection
+ versionMismatch?: {
+ runVersion: number;
+ currentVersion: number;
+ } | null;
}
export default function MiniHistory({
@@ -298,6 +306,7 @@ export default function MiniHistory({
onRetry,
variant = 'floating',
onBack,
+ versionMismatch,
}: MiniHistoryProps) {
const [expandedWorder, setExpandedWorder] = useState('');
const now = new Date();
@@ -305,6 +314,14 @@ export default function MiniHistory({
// Get project and workflow IDs from state for navigation
const project = useProject();
const workflow = useWorkflowState(state => state.workflow);
+ const handleVersionSelect = useVersionSelect();
+
+ // Handler to navigate to the run's version
+ const handleGoToVersion = () => {
+ if (versionMismatch) {
+ handleVersionSelect(versionMismatch.runVersion);
+ }
+ };
// Clear expanded work order when panel collapses
React.useEffect(() => {
@@ -532,6 +549,16 @@ export default function MiniHistory({
)}
+ {/* Version mismatch banner when collapsed */}
+ {collapsed && versionMismatch && (
+
+ )}
+
)}
+
+ {/* Version mismatch banner at bottom of panel */}
+ {!collapsed && versionMismatch && (
+
+ )}
);
}
diff --git a/assets/js/collaborative-editor/components/diagram/VersionMismatchBanner.tsx b/assets/js/collaborative-editor/components/diagram/VersionMismatchBanner.tsx
index 4eb89b1bde..810664330b 100644
--- a/assets/js/collaborative-editor/components/diagram/VersionMismatchBanner.tsx
+++ b/assets/js/collaborative-editor/components/diagram/VersionMismatchBanner.tsx
@@ -9,58 +9,57 @@
* as the workflow structure may have changed between versions.
*
* Features:
- * - Compact two-line design with version information
- * - Dismissible via X button
- * - Positioned at top-center of canvas
+ * - Compact design with version information
+ * - Action button to navigate to the correct version
+ * - Positioned at bottom of MiniHistory panel
*/
-import { useState } from 'react';
-
import { cn } from '#/utils/cn';
+import { Tooltip } from '../Tooltip';
+
interface VersionMismatchBannerProps {
runVersion: number;
currentVersion: number;
+ onGoToVersion: () => void;
className?: string;
+ compact?: boolean;
}
export function VersionMismatchBanner({
runVersion,
currentVersion,
+ onGoToVersion,
className,
+ compact = false,
}: VersionMismatchBannerProps) {
- const [dismissed, setDismissed] = useState(false);
-
- if (dismissed) {
- return null;
- }
+ const tooltipText = `This run was executed on v${runVersion}, but you're visualizing it on v${currentVersion} of the workflow. Big things may have changed.`;
return (