|
32 | 32 | onInspect?: () => Promise<WorkspaceShareView | null>; |
33 | 33 | onCreate?: (request: WorkspaceShareRequest) => Promise<WorkspaceShareView>; |
34 | 34 | onStop?: () => Promise<void>; |
| 35 | + /** True while another tab holds the workspace writer lease — sharing |
| 36 | + * publishes from the editing tab, so creation is gated with a claim |
| 37 | + * action instead of failing into a resume loop. */ |
| 38 | + ownershipBlocked?: boolean; |
| 39 | + /** Attempt to claim the writer lease on this tab; true on success. */ |
| 40 | + onClaimOwnership?: () => Promise<boolean>; |
35 | 41 | onclose: () => void; |
36 | 42 | /** Temporary compatibility for callers that have not moved to WorkspaceDetail yet. */ |
37 | 43 | workspaceName?: string; |
|
46 | 52 | onInspect, |
47 | 53 | onCreate, |
48 | 54 | onStop, |
| 55 | + ownershipBlocked = false, |
| 56 | + onClaimOwnership, |
49 | 57 | onclose, |
50 | 58 | workspaceName, |
51 | 59 | }: Props = $props(); |
|
86 | 94 | let inviteOptionsOpen = $state(false); |
87 | 95 | let stopConfirm = $state(false); |
88 | 96 | let stopBusy = $state(false); |
| 97 | + let claimBusy = $state(false); |
| 98 | +
|
| 99 | + async function claimOwnership(): Promise<void> { |
| 100 | + if (!onClaimOwnership || claimBusy) return; |
| 101 | + claimBusy = true; |
| 102 | + try { |
| 103 | + const granted = await onClaimOwnership(); |
| 104 | + if (!granted) { |
| 105 | + statusMessage = 'The other tab is still editing. Close it (or finish there) and try again.'; |
| 106 | + } else { |
| 107 | + statusMessage = 'This tab can publish now.'; |
| 108 | + } |
| 109 | + } finally { |
| 110 | + claimBusy = false; |
| 111 | + } |
| 112 | + } |
89 | 113 | let stopSharingButton = $state<HTMLButtonElement | undefined>(); |
90 | 114 | let keepSharingButton = $state<HTMLButtonElement | undefined>(); |
91 | 115 |
|
|
427 | 451 | {#if operationError} |
428 | 452 | <p class="share-error" role="alert">{operationError}</p> |
429 | 453 | {/if} |
430 | | - <div class="share-config-foot"> |
431 | | - <p>{configurationHint}</p> |
432 | | - <button class="button primary" type="button" disabled={!configurationReady} onclick={() => void publishShare()}> |
433 | | - Create review link for {manifest.entryCount} {manifest.entryCount === 1 ? 'file' : 'files'} |
434 | | - </button> |
435 | | - </div> |
| 454 | + {#if ownershipBlocked} |
| 455 | + <div class="share-ownership-note" role="status" data-slot="share-ownership-blocked"> |
| 456 | + <p> |
| 457 | + <strong>Another tab is editing this workspace.</strong> |
| 458 | + Sharing publishes from the editing tab — switch to it, or close it |
| 459 | + and claim editing here. |
| 460 | + </p> |
| 461 | + <button class="button" type="button" disabled={claimBusy || !onClaimOwnership} onclick={() => void claimOwnership()}> |
| 462 | + {claimBusy ? 'Checking…' : 'Claim editing here'} |
| 463 | + </button> |
| 464 | + </div> |
| 465 | + {:else} |
| 466 | + <div class="share-config-foot"> |
| 467 | + <p>{configurationHint}</p> |
| 468 | + <button class="button primary" type="button" disabled={!configurationReady} onclick={() => void publishShare()}> |
| 469 | + Create review link for {manifest.entryCount} {manifest.entryCount === 1 ? 'file' : 'files'} |
| 470 | + </button> |
| 471 | + </div> |
| 472 | + {/if} |
436 | 473 | {:else if phase === 'progress'} |
437 | 474 | <div class="share-progress share-progress-compact" aria-live="polite"> |
438 | 475 | {#if operationBusy} |
|
448 | 485 | <p class="share-error" role="alert">{operationError}</p> |
449 | 486 | {/if} |
450 | 487 | {#if progressPaused} |
| 488 | + {#if ownershipBlocked} |
| 489 | + <div class="share-ownership-note" role="status" data-slot="share-ownership-blocked"> |
| 490 | + <p> |
| 491 | + <strong>Another tab is editing this workspace.</strong> |
| 492 | + Resume from that tab, or claim editing here first. |
| 493 | + </p> |
| 494 | + <button class="button" type="button" disabled={claimBusy || !onClaimOwnership} onclick={() => void claimOwnership()}> |
| 495 | + {claimBusy ? 'Checking…' : 'Claim editing here'} |
| 496 | + </button> |
| 497 | + </div> |
| 498 | + {/if} |
451 | 499 | <div class="share-foot"> |
452 | 500 | <button class="button" type="button" disabled={!onStop || stopBusy} onclick={() => void stopSharing()}> |
453 | 501 | {stopBusy ? 'Discarding…' : 'Discard and start over'} |
454 | 502 | </button> |
455 | | - <button class="button primary" type="button" disabled={!onCreate || operationBusy} onclick={() => void publishShare()}>Resume publishing</button> |
| 503 | + <button class="button primary" type="button" disabled={!onCreate || operationBusy || ownershipBlocked} onclick={() => void publishShare()}>Resume publishing</button> |
456 | 504 | </div> |
457 | 505 | {/if} |
458 | 506 | {:else if phase === 'ready' && invite && share} |
|
0 commit comments