@@ -21,6 +21,17 @@ import { Commands } from './commands';
2121import { competitions } from './pages/competitions' ;
2222import { notebookPlugin } from './pages/notebook' ;
2323
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+ }
34+
2435/**
2536 * Get the current notebook panel
2637 */
@@ -59,8 +70,112 @@ const plugin: JupyterFrontEndPlugin<void> = {
5970 const apiUrl =
6071 PageConfig . getOption ( 'sharing_service_api_url' ) || 'http://localhost:8080/api/v1' ;
6172
73+ const notebookPasswords = new Map ( ) ;
6274 const sharingService = new SharingService ( apiUrl ) ;
6375
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+
167+ /**
168+ * Hook into notebook saves using the saveState signal to handle CKHub sharing
169+ */
170+ tracker . widgetAdded . connect ( ( sender , widget ) => {
171+ widget . context . saveState . connect ( async ( sender , saveState ) => {
172+ // Only trigger when save is completed (not dirty and not saving)
173+ if ( saveState === 'completed' ) {
174+ await handleNotebookSave ( widget , false ) ;
175+ }
176+ } ) ;
177+ } ) ;
178+
64179 /**
65180 * 1. A "Download as IPyNB" command.
66181 */
@@ -114,102 +229,72 @@ const plugin: JupyterFrontEndPlugin<void> = {
114229
115230 const notebookContent = notebookPanel . context . model . toJSON ( ) as INotebookContent ;
116231
117- // Check if notebook has already been shared; access metadata using notebook content
118- let notebookId : string | undefined ;
119- if (
120- notebookContent . metadata &&
121- typeof notebookContent . metadata === 'object' &&
122- 'sharedId' in notebookContent . metadata
123- ) {
124- notebookId = notebookContent . metadata . sharedId as string ;
125- }
232+ // Check if notebook has already been shared
233+ const notebookId = notebookContent ?. metadata ?. sharedId as string ;
126234
127235 const isNewShare = ! notebookId ;
128236
129- const result = await showDialog ( {
130- title : isNewShare ? 'Share Notebook' : 'Update Shared Notebook' ,
131- body : new ShareDialog ( ) ,
132- buttons : [ Dialog . cancelButton ( ) , Dialog . okButton ( ) ]
133- } ) ;
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+ } ) ;
134244
135- if ( result . button . accept ) {
136- const { notebookName, password } = result . value as IShareDialogData ;
137-
138- try {
139- // Show loading indicator
140- // TODO: this doesn't show up in the dialog properly, we could
141- // even remove it as loading doesn't take long at all
142- const loadingIndicator = document . createElement ( 'div' ) ;
143- loadingIndicator . textContent = 'Sharing notebook...' ;
144- loadingIndicator . style . position = 'fixed' ;
145- loadingIndicator . style . bottom = '20px' ;
146- loadingIndicator . style . right = '20px' ;
147- loadingIndicator . style . padding = '10px' ;
148- loadingIndicator . style . backgroundColor = '#f0f0f0' ;
149- loadingIndicator . style . borderRadius = '5px' ;
150- loadingIndicator . style . zIndex = '1000' ;
151- document . body . appendChild ( loadingIndicator ) ;
152-
153- await sharingService . authenticate ( ) ;
154-
155- let shareResponse ;
156- if ( isNewShare ) {
157- shareResponse = await sharingService . share ( notebookContent , password ) ;
158- } else if ( notebookId ) {
159- shareResponse = await sharingService . update ( notebookId , notebookContent , password ) ;
160- }
245+ if ( result . button . accept ) {
246+ const { notebookName } = result . value as IShareDialogData ;
247+ const password = generatePassword ( ) ;
161248
162- if ( shareResponse && shareResponse . notebook ) {
163- // We need to update the metadata in the notebookContent first
164- // to do this, and we need to ensure that the metadata object exists
165- if ( ! notebookContent . metadata ) {
166- notebookContent . metadata = { } ;
167- }
249+ try {
250+ const shareResponse = await sharingService . share ( notebookContent , password ) ;
168251
169- notebookContent . metadata . sharedId = shareResponse . notebook . id ;
170- notebookContent . metadata . readableId = shareResponse . notebook . readable_id ;
171- notebookContent . metadata . sharedName = notebookName ;
172- notebookContent . metadata . isPasswordProtected = true ;
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+ } ;
173261
174- notebookPanel . context . model . fromJSON ( notebookContent ) ;
175- }
262+ notebookPanel . context . model . fromJSON ( notebookContent ) ;
263+ await notebookPanel . context . save ( ) ;
176264
177- let shareableLink = '' ;
178- if ( shareResponse && shareResponse . notebook ) {
179- const id = shareResponse . notebook . readable_id || shareResponse . notebook . id ;
180- shareableLink = sharingService . makeRetrieveURL ( id ) . toString ( ) ;
181- }
265+ const id = shareResponse . notebook . readable_id || shareResponse . notebook . id ;
266+ shareableLink = sharingService . makeRetrieveURL ( id ) . toString ( ) ;
267+ notebookPasswords . set ( shareResponse . notebook . id , password ) ;
268+ }
182269
183- // Remove loading indicator
184- document . body . removeChild ( loadingIndicator ) ;
185-
186- if ( shareableLink ) {
187- const dialogResult = await showDialog ( {
188- title : isNewShare
189- ? 'Notebook Shared Successfully'
190- : 'Notebook Updated Successfully' ,
191- body : ReactWidget . create ( createSuccessDialog ( shareableLink , isNewShare , true ) ) ,
192- buttons : [
193- Dialog . okButton ( { label : 'Copy Link' } ) ,
194- Dialog . cancelButton ( { label : 'Close' } )
195- ]
196- } ) ;
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+ } ) ;
197279
198- if ( dialogResult . button . label === 'Copy Link' ) {
199- try {
200- await navigator . clipboard . writeText ( shareableLink ) ;
201- } catch ( err ) {
202- console . error ( 'Failed to copy link:' , err ) ;
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+ }
203286 }
204287 }
288+ } catch ( error ) {
289+ await showDialog ( {
290+ title : 'Error' ,
291+ body : ReactWidget . create ( createErrorDialog ( error ) ) ,
292+ buttons : [ Dialog . okButton ( ) ]
293+ } ) ;
205294 }
206- } catch ( error ) {
207- await showDialog ( {
208- title : 'Error' ,
209- body : ReactWidget . create ( createErrorDialog ( error ) ) ,
210- buttons : [ Dialog . okButton ( ) ]
211- } ) ;
212295 }
296+ } else {
297+ await handleNotebookSave ( notebookPanel , true ) ;
213298 }
214299 } catch ( error ) {
215300 console . error ( 'Error in share command:' , error ) ;
0 commit comments