@@ -10,7 +10,7 @@ import { Icon } from "@blueprintjs/core";
1010import ExtensionApiContextProvider , {
1111 useExtensionAPI ,
1212} from "roamjs-components/components/ExtensionApiContext" ;
13- import { OnloadArgs } from "roamjs-components/types" ;
13+ import { OnloadArgs , PullBlock } from "roamjs-components/types" ;
1414import renderWithUnmount from "roamjs-components/util/renderWithUnmount" ;
1515
1616import {
@@ -102,9 +102,12 @@ import { BLOCK_REF_REGEX } from "roamjs-components/dom";
102102import { defaultHandleExternalTextContent } from "./defaultHandleExternalTextContent" ;
103103import {
104104 CanvasSyncMode ,
105+ getCanvasSyncMigrationState ,
106+ getReadyCanvasStore ,
105107 ensureCanvasSyncMode ,
106108 getEffectiveCanvasSyncMode ,
107- setCanvasSyncMode ,
109+ migrateLocalCanvasToCloud ,
110+ setCanvasSyncSettings ,
108111} from "./canvasSyncMode" ;
109112import {
110113 CanvasStoreAdapterArgs ,
@@ -113,6 +116,7 @@ import {
113116import posthog from "posthog-js" ;
114117import { getPersonalSetting } from "~/components/settings/utils/accessors" ;
115118import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys" ;
119+ import { json , normalizeProps } from "~/utils/getBlockProps" ;
116120
117121declare global {
118122 // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
@@ -171,9 +175,44 @@ const TldrawCanvas = ({ title }: { title: string }) => {
171175 setCanvasSyncModeState ( ensureCanvasSyncMode ( { pageUid } ) ) ;
172176 } , [ pageUid ] ) ;
173177
178+ // Convert from local to sync when the block props change
179+ // EG: When UserA sets the canvas sync mode to sync
180+ // UserB's canvas should be converted to sync
181+ useEffect ( ( ) => {
182+ if ( ! pageUid || canvasSyncMode !== "local" ) return ;
183+
184+ const pullWatchProps = [
185+ "[:block/props]" ,
186+ `[:block/uid "${ pageUid } "]` ,
187+ ( _before : PullBlock | null , after : PullBlock | null ) => {
188+ const blockProps = normalizeProps (
189+ ( after ?. [ ":block/props" ] || { } ) as json ,
190+ ) as Record < string , json > ;
191+ const rjsqb = blockProps ?. [ "roamjs-query-builder" ] as Record <
192+ string ,
193+ unknown
194+ > ;
195+ if ( rjsqb ?. canvasSyncMode !== "sync" ) return ;
196+ setCanvasSyncModeState ( "sync" ) ;
197+ } ,
198+ ] as const ;
199+
200+ window . roamAlphaAPI . data . addPullWatch ( ...pullWatchProps ) ;
201+ return ( ) => {
202+ window . roamAlphaAPI . data . removePullWatch ( ...pullWatchProps ) ;
203+ } ;
204+ } , [ canvasSyncMode , pageUid ] ) ;
205+
174206 const onCanvasSyncModeChange = useCallback (
175207 ( mode : CanvasSyncMode ) => {
176- setCanvasSyncMode ( { pageUid, mode } ) ;
208+ const currentMigrationState = getCanvasSyncMigrationState ( { pageUid } ) ;
209+ const nextMigrationState =
210+ mode === "sync" && ! currentMigrationState ? "pending" : undefined ;
211+ setCanvasSyncSettings ( {
212+ pageUid,
213+ mode,
214+ migrationState : nextMigrationState ,
215+ } ) ;
177216 setCanvasSyncModeState ( mode ) ;
178217 } ,
179218 [ pageUid ] ,
@@ -254,6 +293,7 @@ const useCloudflareCanvasStore = ({
254293 performUpgrade : ( ) => { } ,
255294 } ;
256295} ;
296+
257297const TldrawCanvasRoam = ( {
258298 title,
259299 pageUid,
@@ -632,7 +672,6 @@ const TldrawCanvasShared = ({
632672 allRelationNames,
633673 allAddReferencedNodeActions,
634674 canvasSyncMode,
635- onCanvasSyncModeChange,
636675 } ) ;
637676
638677 const storeAdapterArgs = useCanvasStoreAdapterArgs ( {
@@ -643,6 +682,7 @@ const TldrawCanvasShared = ({
643682 allAddReferencedNodeByAction,
644683 } ) ;
645684 const {
685+ migrations,
646686 customShapeUtils,
647687 customBindingUtils,
648688 customShapeTypes,
@@ -671,6 +711,8 @@ const TldrawCanvasShared = ({
671711 allNodes,
672712 allRelationNames,
673713 allAddReferencedNodeByAction,
714+ canvasSyncMode,
715+ onCanvasSyncModeChange,
674716 toggleMaximized : handleMaximizedChange ,
675717 setConvertToDialogOpen,
676718 discourseContext,
@@ -685,6 +727,55 @@ const TldrawCanvasShared = ({
685727 } , [ pageUid ] ) ;
686728 const { store, needsUpgrade, performUpgrade, error, isLoading } =
687729 useStoreAdapter ( storeAdapterArgs ) ;
730+ const migratedCloudStoreRef = useRef < string | null > ( null ) ;
731+
732+ // Migrate local canvas to cloud sync
733+ useEffect ( ( ) => {
734+ if ( ! isCloudflareSync || canvasSyncMode !== "sync" ) return ;
735+ if ( getCanvasSyncMigrationState ( { pageUid } ) !== "pending" ) return ;
736+
737+ const readyStore = getReadyCanvasStore ( store ) ;
738+ if ( ! readyStore ) return ;
739+
740+ const storeId = `${ pageUid } :${ readyStore . id } ` ;
741+ if ( migratedCloudStoreRef . current === storeId ) return ;
742+ migratedCloudStoreRef . current = storeId ;
743+
744+ try {
745+ const { migrated } = migrateLocalCanvasToCloud ( {
746+ pageUid,
747+ store : readyStore ,
748+ migrations,
749+ customShapeUtils,
750+ customBindingUtils,
751+ } ) ;
752+ if ( migrated )
753+ renderToast ( {
754+ id : "tldraw-cloud-migration" ,
755+ intent : "success" ,
756+ content : "Migrated local canvas to cloud sync." ,
757+ } ) ;
758+ } catch ( migrationError ) {
759+ migratedCloudStoreRef . current = null ;
760+ internalError ( {
761+ error : migrationError ,
762+ type : "Canvas: Local to cloud migration failed" ,
763+ context : {
764+ pageUid,
765+ title,
766+ } ,
767+ } ) ;
768+ }
769+ } , [
770+ canvasSyncMode ,
771+ customBindingUtils ,
772+ customShapeUtils ,
773+ isCloudflareSync ,
774+ migrations ,
775+ pageUid ,
776+ store ,
777+ title ,
778+ ] ) ;
688779
689780 // ASSETS
690781 const assetLoading = usePreloadAssets ( defaultEditorAssetUrls ) ;
0 commit comments