diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/cleared-refs-list.test.ts b/apps/sim/ee/workspace-forking/components/fork-sync/cleared-refs-list.test.ts index fce97ef41ca..aab10731aa4 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/cleared-refs-list.test.ts +++ b/apps/sim/ee/workspace-forking/components/fork-sync/cleared-refs-list.test.ts @@ -149,21 +149,36 @@ describe('splitForkClearedRefs', () => { describe('forkBlockerResolution', () => { it('phrases each blocker reason with its actionable resolution', () => { - expect(forkBlockerResolution(referenceRef('table', 'tbl-1'))).toBe( + expect(forkBlockerResolution(referenceRef('table', 'tbl-1'), 'Acme Prod')).toBe( 'map it to a target or select it for copy' ) - expect(forkBlockerResolution(referenceRef('mcp-server', 'srv-1'))).toBe( + expect(forkBlockerResolution(referenceRef('mcp-server', 'srv-1'), 'Acme Prod')).toBe( 'map it to a target or select it for copy' ) - expect(forkBlockerResolution(referenceRef('knowledge-base', 'kb-gone', 'KB', true))).toBe( - 'deleted in the source — map it to an existing knowledge base in the target' - ) - expect(forkBlockerResolution(workflowRef('wf-other', 'Workflow'))).toBe( + expect( + forkBlockerResolution(referenceRef('knowledge-base', 'kb-gone', 'KB', true), 'Acme Prod') + ).toBe('deleted in the source — map it to an existing knowledge base in Acme Prod') + expect(forkBlockerResolution(workflowRef('wf-other', 'Workflow'), 'Acme Prod')).toBe( 'deploy "Source" in the source or remove the reference' ) }) + /** + * The source-deleted line phrases the same resolution as the mapping row's hint, so it must + * name the workspace the sync writes - "the target" is what this copy set out to remove. + */ + it('names the target workspace in the source-deleted resolution', () => { + const resolution = forkBlockerResolution( + referenceRef('knowledge-base', 'kb-gone', 'KB', true), + 'this workspace' + ) + expect(resolution).toBe( + 'deleted in the source — map it to an existing knowledge base in this workspace' + ) + expect(resolution).not.toContain('in the target') + }) + it('returns null for non-blocking dependent entries', () => { - expect(forkBlockerResolution(dependentRef('credential', 'cred-1'))).toBeNull() + expect(forkBlockerResolution(dependentRef('credential', 'cred-1'), 'Acme Prod')).toBeNull() }) }) diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/cleared-refs-list.ts b/apps/sim/ee/workspace-forking/components/fork-sync/cleared-refs-list.ts index 0e928112356..6bf7fa453b2 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/cleared-refs-list.ts +++ b/apps/sim/ee/workspace-forking/components/fork-sync/cleared-refs-list.ts @@ -77,15 +77,24 @@ export const FORK_RESOURCE_KIND_LABEL: Record = { /** * The actionable resolution line for a blocking entry, phrased for "{block} would lose {field} * in {workflow} - {resolution}". Null for non-blocking (dependent) entries. + * + * `targetWorkspaceName` is required rather than defaulted: the source-deleted line phrases the + * same resolution as the mapping row's own hint, and naming the workspace is the only way either + * says WHICH side the sync writes. A default would let "the target" quietly return. */ -export function forkBlockerResolution(ref: ForkClearedRef): string | null { +export function forkBlockerResolution( + ref: ForkClearedRef, + targetWorkspaceName: string +): string | null { const reason = forkSyncBlockerReasonFor(ref) if (!reason) return null switch (reason) { + // "a target" here is the target RESOURCE picked in the mapping row, not the workspace - + // it matches the picker's own "Select target" label, so it stays unnamed. case 'unmapped-copyable': return 'map it to a target or select it for copy' case 'source-deleted': - return `deleted in the source — map it to an existing ${FORK_RESOURCE_KIND_LABEL[ref.kind] ?? 'resource'} in the target` + return `deleted in the source — map it to an existing ${FORK_RESOURCE_KIND_LABEL[ref.kind] ?? 'resource'} in ${targetWorkspaceName}` case 'workflow-missing': return `deploy "${ref.sourceLabel}" in the source or remove the reference` } diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx index 9a27dbf54eb..487255b5839 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx @@ -457,8 +457,8 @@ function MappingEntry({ controller, group, entry }: MappingEntryProps) { {entry.sourceDeleted ? (

Deleted in the source — its name can't be shown. Map it to an existing{' '} - {FORK_RESOURCE_KIND_LABEL[entry.kind] ?? 'resource'} in the target, or fix the reference - in the source and redeploy. + {FORK_RESOURCE_KIND_LABEL[entry.kind] ?? 'resource'} in {controller.targetWorkspaceName} + , or fix the reference in the source and redeploy.

) : null} {entry.candidatesTruncated ? ( @@ -961,7 +961,8 @@ export function ForkSyncView({ controller, onDirectionChange }: ForkSyncViewProp {ref.blockLabel} would lose{' '} {ref.fieldLabel} in{' '} - {ref.workflowName} — {forkBlockerResolution(ref)} + {ref.workflowName} —{' '} + {forkBlockerResolution(ref, controller.targetWorkspaceName)} {/* Only a source-deleted reference can be dropped: an unmapped copyable can still be copied and a missing workflow can still be deployed, so neither is a dead diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts b/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts index 61aadc7bce5..f5c89c22112 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts +++ b/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts @@ -104,8 +104,10 @@ export interface ForkSyncController { otherWorkspaceName: string /** * The workspace this sync WRITES, named for user-facing copy: the other workspace on push, - * "this workspace" on pull. Derived once here so every surface that names the target - the - * overwrite confirm, the Trigger URLs heading - says the same thing. + * this one on pull. Always a NAME rather than "the target" - the page header shows the OTHER + * workspace's name, so an unnamed target reads as that one even on pull. Falls back to + * "this workspace" only until the name loads. Derived once here so every surface that names + * it - the overwrite confirm, the Trigger URLs heading - says the same thing. */ targetWorkspaceName: string isLoading: boolean @@ -272,12 +274,15 @@ function takenTargetOwners( */ export function useForkSync(params: { workspaceId: string + /** This workspace's name, for copy that must say which side a pull overwrites. */ + workspaceName?: string otherWorkspaceId?: string otherWorkspaceName: string direction: ForkDirection enabled: boolean }): ForkSyncController { - const { workspaceId, otherWorkspaceId, otherWorkspaceName, direction, enabled } = params + const { workspaceId, workspaceName, otherWorkspaceId, otherWorkspaceName, direction, enabled } = + params // User's IN-SESSION mapping overrides only - NOT the source of truth. The displayed/persisted // target falls back to each entry's stored `targetId` (see `targetFor`), so a reopened edge @@ -982,7 +987,8 @@ export function useForkSync(params: { return { direction, otherWorkspaceName, - targetWorkspaceName: direction === 'push' ? otherWorkspaceName : 'this workspace', + targetWorkspaceName: + direction === 'push' ? otherWorkspaceName : workspaceName || 'this workspace', isLoading: enabled && mapping.isLoading, isError: mapping.isError, errorMessage: mapping.isError ? getErrorMessage(mapping.error, 'Failed to load mapping') : null, diff --git a/apps/sim/ee/workspace-forking/components/forks.tsx b/apps/sim/ee/workspace-forking/components/forks.tsx index 61026ca0ffb..7acc581eba9 100644 --- a/apps/sim/ee/workspace-forking/components/forks.tsx +++ b/apps/sim/ee/workspace-forking/components/forks.tsx @@ -85,6 +85,8 @@ function ForkListRow({ name, actions }: ForkListRowProps) { interface ForkSyncDetailViewProps { title: string workspaceId: string + /** This workspace's name — a pull overwrites it, and the copy has to say which side that is. */ + workspaceName?: string /** The other side of the edge being synced (this workspace's parent). */ otherWorkspaceId: string otherWorkspaceName: string @@ -105,6 +107,7 @@ interface ForkSyncDetailViewProps { function ForkSyncDetailView({ title, workspaceId, + workspaceName, otherWorkspaceId, otherWorkspaceName, onBack, @@ -118,6 +121,7 @@ function ForkSyncDetailView({ const controller = useForkSync({ workspaceId, + workspaceName, otherWorkspaceId, otherWorkspaceName, direction, @@ -186,11 +190,11 @@ function ForkSyncDetailView({ open={confirmSyncOpen} onOpenChange={setConfirmSyncOpen} srTitle='Sync workspace' - title='Overwrite target workspace' + title={`Overwrite ${targetWorkspaceName}`} text={[ - 'The target may have been modified since the last sync. Syncing will ', + 'Syncing will ', { text: 'overwrite any changes', bold: true }, - ' there. Continue?', + ` made in ${targetWorkspaceName} since the last sync. Continue?`, ]} confirm={{ label: 'Sync', @@ -431,6 +435,7 @@ export function Forks() { key={parent.id} title={parent.name} workspaceId={workspaceId} + workspaceName={workspaceName} otherWorkspaceId={parent.id} otherWorkspaceName={parent.name} onBack={() => void setSelectedForkId(null, { history: 'replace' })}