@@ -8,29 +8,14 @@ import { INotebookContent } from '@jupyterlab/nbformat';
88import { customSidebar } from './sidebar' ;
99import { SharingService } from './sharing-service' ;
1010
11- import {
12- IShareDialogData ,
13- ShareDialog ,
14- createSuccessDialog ,
15- createErrorDialog
16- } from './ui-components/share-dialog' ;
11+ import { createSuccessDialog , createErrorDialog } from './ui-components/share-dialog' ;
1712
1813import { exportNotebookAsPDF } from './pdf' ;
1914import { files } from './pages/files' ;
2015import { Commands } from './commands' ;
2116import { competitions } from './pages/competitions' ;
2217import { notebookPlugin } from './pages/notebook' ;
23-
24- function generatePassword ( ) : string {
25- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*' ;
26- const array = new Uint8Array ( 16 ) ;
27- crypto . getRandomValues ( array ) ;
28- let password = '' ;
29- for ( let i = 0 ; i < array . length ; i ++ ) {
30- password += chars . charAt ( array [ i ] % chars . length ) ;
31- }
32- return password ;
33- }
18+ import { generateDefaultNotebookName } from './notebook-name' ;
3419
3520/**
3621 * Get the current notebook panel
@@ -50,6 +35,84 @@ function getCurrentNotebook(
5035 return widget ;
5136}
5237
38+ const manuallySharing = new WeakSet < NotebookPanel > ( ) ;
39+
40+ /**
41+ * Show a dialog with a shareable link for the notebook.
42+ * @param sharingService - The sharing service instance to use for generating the shareable link.
43+ * @param notebookContent - The content of the notebook to share, from which we extract the ID.
44+ */
45+ async function showShareDialog ( sharingService : SharingService , notebookContent : INotebookContent ) {
46+ const id = ( notebookContent . metadata . readableId || notebookContent . metadata . sharedId ) as string ;
47+ const shareableLink = sharingService . makeRetrieveURL ( id ) . toString ( ) ;
48+
49+ const dialogResult = await showDialog ( {
50+ title : '' ,
51+ body : ReactWidget . create ( createSuccessDialog ( shareableLink ) ) ,
52+ buttons : [ Dialog . okButton ( { label : 'Copy Link!' } ) , Dialog . cancelButton ( { label : 'Close' } ) ]
53+ } ) ;
54+
55+ if ( dialogResult . button . label === 'Copy Link!' ) {
56+ try {
57+ await navigator . clipboard . writeText ( shareableLink ) ;
58+ } catch ( err ) {
59+ console . error ( 'Failed to copy link:' , err ) ;
60+ }
61+ }
62+ }
63+
64+ /**
65+ * Notebook share/save handler. This function handles both sharing a new notebook and
66+ * updating an existing shared notebook.
67+ * @param notebookPanel - The notebook panel to handle sharing for.
68+ * @param sharingService - The sharing service instance to use for sharing operations.
69+ * @param manual - Whether this is a manual share operation triggered by the user, i.e., it is
70+ * true when the user clicks "Share Notebook" from the menu.
71+ */
72+ async function handleNotebookSharing (
73+ notebookPanel : NotebookPanel ,
74+ sharingService : SharingService ,
75+ manual : boolean
76+ ) {
77+ const notebookContent = notebookPanel . context . model . toJSON ( ) as INotebookContent ;
78+
79+ const sharedId = notebookContent . metadata ?. sharedId as string | undefined ;
80+ const defaultName = generateDefaultNotebookName ( ) ;
81+
82+ try {
83+ if ( sharedId ) {
84+ console . log ( 'Updating notebook:' , sharedId ) ;
85+ await sharingService . update ( sharedId , notebookContent ) ;
86+
87+ console . log ( 'Notebook automatically synced to CKHub' ) ;
88+ } else {
89+ const shareResponse = await sharingService . share ( notebookContent ) ;
90+
91+ notebookContent . metadata = {
92+ ...notebookContent . metadata ,
93+ sharedId : shareResponse . notebook . id ,
94+ readableId : shareResponse . notebook . readable_id ,
95+ sharedName : defaultName ,
96+ lastShared : new Date ( ) . toISOString ( )
97+ } ;
98+
99+ notebookPanel . context . model . fromJSON ( notebookContent ) ;
100+ await notebookPanel . context . save ( ) ;
101+ }
102+
103+ if ( manual ) {
104+ await showShareDialog ( sharingService , notebookContent ) ;
105+ }
106+ } catch ( error ) {
107+ console . warn ( 'Failed to sync notebook to CKHub:' , error ) ;
108+ await showDialog ( {
109+ title : manual ? 'Error Sharing Notebook' : 'Sync Failed' ,
110+ body : ReactWidget . create ( createErrorDialog ( error ) ) ,
111+ buttons : [ Dialog . okButton ( ) ]
112+ } ) ;
113+ }
114+ }
115+
53116/**
54117 * JUPYTEREVERYWHERE EXTENSION
55118 */
@@ -70,108 +133,20 @@ const plugin: JupyterFrontEndPlugin<void> = {
70133 const apiUrl =
71134 PageConfig . getOption ( 'sharing_service_api_url' ) || 'http://localhost:8080/api/v1' ;
72135
73- const notebookPasswords = new Map ( ) ;
74136 const sharingService = new SharingService ( apiUrl ) ;
75137
76- async function handleNotebookSave (
77- notebookPanel : NotebookPanel ,
78- isManualShare : boolean = false
79- ) {
80- const notebookContent = notebookPanel . context . model . toJSON ( ) as INotebookContent ;
81-
82- // Check if notebook has already been shared
83- const isAlreadyShared =
84- notebookContent . metadata &&
85- typeof notebookContent . metadata === 'object' &&
86- 'sharedId' in notebookContent . metadata ;
87-
88- if ( isAlreadyShared && ! isManualShare ) {
89- try {
90- const sharedId = notebookContent . metadata . sharedId as string ;
91- console . log ( 'Updating notebook:' , sharedId ) ;
92-
93- await sharingService . update ( sharedId , notebookContent ) ;
94-
95- console . log ( 'Notebook automatically synced to CKHub' ) ;
96- } catch ( error ) {
97- console . warn ( 'Failed to sync notebook to CKHub:' , error ) ;
98- await showDialog ( {
99- title : 'Sync Failed' ,
100- body : ReactWidget . create ( createErrorDialog ( error ) ) ,
101- buttons : [ Dialog . okButton ( ) ]
102- } ) ;
103- }
104- return ;
105- }
106-
107- if ( ! isAlreadyShared && ! isManualShare ) {
108- // First save/share; displays a shareable link and shows a password in a dialog
109- const password = generatePassword ( ) ;
110- const defaultName = `Notebook_${ new Date ( ) . getFullYear ( ) } -${ ( new Date ( ) . getMonth ( ) + 1 ) . toString ( ) . padStart ( 2 , '0' ) } -${ new Date ( ) . getDate ( ) . toString ( ) . padStart ( 2 , '0' ) } ` ;
111-
112- try {
113- const shareResponse = await sharingService . share ( notebookContent , password ) ;
114-
115- if ( shareResponse && shareResponse . notebook ) {
116- notebookContent . metadata = {
117- ...notebookContent . metadata ,
118- sharedId : shareResponse . notebook . id ,
119- readableId : shareResponse . notebook . readable_id ,
120- sharedName : defaultName ,
121- isPasswordProtected : true ,
122- lastShared : new Date ( ) . toISOString ( )
123- } ;
124-
125- notebookPasswords . set ( shareResponse . notebook . id , password ) ;
126-
127- notebookPanel . context . model . fromJSON ( notebookContent ) ;
128- await notebookPanel . context . save ( ) ;
129- }
130- } catch ( error ) {
131- console . error ( 'Failed to share notebook:' , error ) ;
132- await showDialog ( {
133- title : 'Error Sharing Notebook' ,
134- body : ReactWidget . create ( createErrorDialog ( error ) ) ,
135- buttons : [ Dialog . okButton ( ) ]
136- } ) ;
137- }
138- }
139-
140- if ( isManualShare ) {
141- // Manual share button pressed - show link and password
142- const readableId = notebookContent . metadata . readableId as string ;
143- const sharedId = notebookContent . metadata . sharedId as string ;
144- const shareableLink = sharingService . makeRetrieveURL ( readableId || sharedId ) . toString ( ) ;
145-
146- const dialogResult = await showDialog ( {
147- title : '' ,
148- body : ReactWidget . create (
149- createSuccessDialog ( shareableLink , notebookPasswords . get ( sharedId ) )
150- ) ,
151- buttons : [
152- Dialog . okButton ( { label : 'Copy Link!' } ) ,
153- Dialog . cancelButton ( { label : 'Close' } )
154- ]
155- } ) ;
156-
157- if ( dialogResult . button . label === 'Copy Link!' ) {
158- try {
159- await navigator . clipboard . writeText ( shareableLink ) ;
160- } catch ( err ) {
161- console . error ( 'Failed to copy link:' , err ) ;
162- }
163- }
164- }
165- }
166-
167138 /**
168139 * Hook into notebook saves using the saveState signal to handle CKHub sharing
169140 */
170141 tracker . widgetAdded . connect ( ( sender , widget ) => {
171142 widget . context . saveState . connect ( async ( sender , saveState ) => {
172143 // Only trigger when save is completed (not dirty and not saving)
173144 if ( saveState === 'completed' ) {
174- await handleNotebookSave ( widget , false ) ;
145+ if ( manuallySharing . has ( widget ) ) {
146+ // Skip auto-sync if it's a manual share.
147+ return ;
148+ }
149+ await handleNotebookSharing ( widget , sharingService , false ) ;
175150 }
176151 } ) ;
177152 } ) ;
@@ -224,78 +199,14 @@ const plugin: JupyterFrontEndPlugin<void> = {
224199 return ;
225200 }
226201
202+ // Mark this notebook as being shared manually (i.e., the user has
203+ // clicked the "Share Notebook" command).
204+ manuallySharing . add ( notebookPanel ) ;
205+
227206 // Save the notebook before we share it.
228207 await notebookPanel . context . save ( ) ;
229208
230- const notebookContent = notebookPanel . context . model . toJSON ( ) as INotebookContent ;
231-
232- // Check if notebook has already been shared
233- const notebookId = notebookContent ?. metadata ?. sharedId as string ;
234-
235- const isNewShare = ! notebookId ;
236-
237- if ( isNewShare ) {
238- // First time sharing - show dialog to get notebook name
239- const result = await showDialog ( {
240- title : 'Share Notebook' ,
241- body : new ShareDialog ( ) ,
242- buttons : [ Dialog . cancelButton ( ) , Dialog . okButton ( ) ]
243- } ) ;
244-
245- if ( result . button . accept ) {
246- const { notebookName } = result . value as IShareDialogData ;
247- const password = generatePassword ( ) ;
248-
249- try {
250- const shareResponse = await sharingService . share ( notebookContent , password ) ;
251-
252- let shareableLink = '' ;
253- if ( shareResponse && shareResponse . notebook ) {
254- notebookContent . metadata = {
255- ...notebookContent . metadata ,
256- sharedId : shareResponse . notebook . id ,
257- readable_id : shareResponse . notebook . readable_id ,
258- sharedName : notebookName ,
259- isPasswordProtected : true
260- } ;
261-
262- notebookPanel . context . model . fromJSON ( notebookContent ) ;
263- await notebookPanel . context . save ( ) ;
264-
265- const id = shareResponse . notebook . readable_id || shareResponse . notebook . id ;
266- shareableLink = sharingService . makeRetrieveURL ( id ) . toString ( ) ;
267- notebookPasswords . set ( shareResponse . notebook . id , password ) ;
268- }
269-
270- if ( shareableLink ) {
271- const dialogResult = await showDialog ( {
272- title : '' ,
273- body : ReactWidget . create ( createSuccessDialog ( shareableLink , password ) ) ,
274- buttons : [
275- Dialog . okButton ( { label : 'Done' } ) ,
276- Dialog . cancelButton ( { label : 'Copy Link' } )
277- ]
278- } ) ;
279-
280- if ( dialogResult . button . label === 'Copy Link' ) {
281- try {
282- await navigator . clipboard . writeText ( shareableLink ) ;
283- } catch ( err ) {
284- console . error ( 'Failed to copy link:' , err ) ;
285- }
286- }
287- }
288- } catch ( error ) {
289- await showDialog ( {
290- title : 'Error' ,
291- body : ReactWidget . create ( createErrorDialog ( error ) ) ,
292- buttons : [ Dialog . okButton ( ) ]
293- } ) ;
294- }
295- }
296- } else {
297- await handleNotebookSave ( notebookPanel , true ) ;
298- }
209+ await handleNotebookSharing ( notebookPanel , sharingService , true ) ;
299210 } catch ( error ) {
300211 console . error ( 'Error in share command:' , error ) ;
301212 }
0 commit comments