@jupyterlab/notebook 4.6.0-alpha.3 → 4.6.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/actions.tsx CHANGED
@@ -11,12 +11,7 @@ import {
11
11
  showDialog,
12
12
  SystemClipboard
13
13
  } from '@jupyterlab/apputils';
14
- import type {
15
- Cell,
16
- CodeCell,
17
- ICellModel,
18
- ICodeCellModel
19
- } from '@jupyterlab/cells';
14
+ import type { Cell, CodeCell, ICodeCellModel } from '@jupyterlab/cells';
20
15
  import {
21
16
  CodeCellModel,
22
17
  isMarkdownCellModel,
@@ -146,11 +141,32 @@ export class NotebookActions {
146
141
  * A namespace for `NotebookActions` static methods.
147
142
  */
148
143
  export namespace NotebookActions {
144
+ const READ_ONLY_ACTION_AUTO_CLOSE = 5000;
145
+
146
+ function notifySplitReadOnlyAction(translator?: ITranslator): void {
147
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
148
+ Notification.error(trans.__('The cell is read-only and cannot be split.'), {
149
+ autoClose: READ_ONLY_ACTION_AUTO_CLOSE
150
+ });
151
+ }
152
+
153
+ function notifyMergeReadOnlyAction(translator?: ITranslator): void {
154
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
155
+ Notification.error(
156
+ trans.__('The cell is read-only and cannot be merged.'),
157
+ {
158
+ autoClose: READ_ONLY_ACTION_AUTO_CLOSE
159
+ }
160
+ );
161
+ }
162
+
149
163
  /**
150
164
  * Split the active cell into two or more cells.
151
165
  *
152
166
  * @param notebook The target notebook widget.
153
167
  *
168
+ * @param translator - Application translator.
169
+ *
154
170
  * #### Notes
155
171
  * It will preserve the existing mode.
156
172
  * The last cell will be activated if no selection is found.
@@ -165,10 +181,17 @@ export namespace NotebookActions {
165
181
  * This action can be undone.
166
182
  * The original cell is preserved to maintain kernel connections.
167
183
  */
168
- export function splitCell(notebook: Notebook): void {
184
+ export function splitCell(
185
+ notebook: Notebook,
186
+ translator?: ITranslator
187
+ ): void {
169
188
  if (!notebook.model || !notebook.activeCell) {
170
189
  return;
171
190
  }
191
+ if (notebook.activeCell.model.getMetadata('editable') === false) {
192
+ notifySplitReadOnlyAction(translator);
193
+ return;
194
+ }
172
195
 
173
196
  const state = Private.getState(notebook);
174
197
  // We force the notebook back in edit mode as splitting a cell
@@ -305,6 +328,8 @@ export namespace NotebookActions {
305
328
  * @param addExtraLine - Whether to add an extra newline between merged cell contents
306
329
  * (true, default) or use only a single newline (false).
307
330
  *
331
+ * @param translator - Application translator.
332
+ *
308
333
  * #### Notes
309
334
  * The widget mode will be preserved.
310
335
  * If only one cell is selected and `mergeAbove` is true, the above cell will be selected.
@@ -317,7 +342,8 @@ export namespace NotebookActions {
317
342
  export function mergeCells(
318
343
  notebook: Notebook,
319
344
  mergeAbove: boolean = false,
320
- addExtraLine: boolean = true
345
+ addExtraLine: boolean = true,
346
+ translator?: ITranslator
321
347
  ): void {
322
348
  if (!notebook.model || !notebook.activeCell) {
323
349
  return;
@@ -331,10 +357,15 @@ export namespace NotebookActions {
331
357
  const primary = notebook.activeCell;
332
358
  const active = notebook.activeCellIndex;
333
359
  const attachments: nbformat.IAttachments = {};
360
+ let hasReadOnlyCell = false;
334
361
 
335
362
  // Get the cells to merge.
336
363
  notebook.widgets.forEach((child, index) => {
337
364
  if (notebook.isSelectedOrActive(child)) {
365
+ if (child.model.getMetadata('editable') === false) {
366
+ hasReadOnlyCell = true;
367
+ return;
368
+ }
338
369
  toMerge.push(child.model.sharedModel.getSource());
339
370
  if (index !== active) {
340
371
  toDelete.push(index);
@@ -349,6 +380,11 @@ export namespace NotebookActions {
349
380
  }
350
381
  });
351
382
 
383
+ if (hasReadOnlyCell) {
384
+ notifyMergeReadOnlyAction(translator);
385
+ return;
386
+ }
387
+
352
388
  // Check for only a single cell selected.
353
389
  if (toMerge.length === 1) {
354
390
  // Merge with the cell above when mergeAbove is true
@@ -357,6 +393,12 @@ export namespace NotebookActions {
357
393
  if (active === 0) {
358
394
  return;
359
395
  }
396
+ if (
397
+ notebook.widgets[active - 1].model.getMetadata('editable') === false
398
+ ) {
399
+ notifyMergeReadOnlyAction(translator);
400
+ return;
401
+ }
360
402
  // Otherwise merge with the previous cell.
361
403
  const cellModel = cells.get(active - 1);
362
404
 
@@ -367,6 +409,12 @@ export namespace NotebookActions {
367
409
  if (active === cells.length - 1) {
368
410
  return;
369
411
  }
412
+ if (
413
+ notebook.widgets[active + 1].model.getMetadata('editable') === false
414
+ ) {
415
+ notifyMergeReadOnlyAction(translator);
416
+ return;
417
+ }
370
418
  // Otherwise merge with the next cell.
371
419
  const cellModel = cells.get(active + 1);
372
420
 
@@ -579,7 +627,7 @@ export namespace NotebookActions {
579
627
 
580
628
  const state = Private.getState(notebook);
581
629
 
582
- Private.changeCellType(notebook, value, translator);
630
+ Private.changeCellType(notebook, value, { translator });
583
631
  void Private.handleState(notebook, state);
584
632
  }
585
633
 
@@ -1532,7 +1580,6 @@ export namespace NotebookActions {
1532
1580
  }
1533
1581
 
1534
1582
  const state = Private.getState(notebook);
1535
-
1536
1583
  notebook.mode = 'command';
1537
1584
  notebook.model.sharedModel.undo();
1538
1585
  notebook.deselectAll();
@@ -1954,15 +2001,12 @@ export namespace NotebookActions {
1954
2001
  }
1955
2002
 
1956
2003
  const state = Private.getState(notebook);
1957
- const cells = notebook.model.cells;
1958
2004
 
1959
2005
  level = Math.min(Math.max(level, 1), 6);
1960
- notebook.widgets.forEach((child, index) => {
1961
- if (notebook.isSelectedOrActive(child)) {
1962
- Private.setMarkdownHeader(cells.get(index), level);
1963
- }
2006
+ Private.changeCellType(notebook, 'markdown', {
2007
+ translator,
2008
+ headingLevel: level
1964
2009
  });
1965
- Private.changeCellType(notebook, 'markdown', translator);
1966
2010
  void Private.handleState(notebook, state);
1967
2011
  }
1968
2012
 
@@ -2257,12 +2301,12 @@ export namespace NotebookActions {
2257
2301
  export function trust(
2258
2302
  notebook: Notebook,
2259
2303
  translator?: ITranslator
2260
- ): Promise<void> {
2304
+ ): Promise<{ trusted: boolean }> {
2261
2305
  translator = translator || nullTranslator;
2262
2306
  const trans = translator.load('jupyterlab');
2263
2307
 
2264
2308
  if (!notebook.model) {
2265
- return Promise.resolve();
2309
+ return Promise.resolve({ trusted: false });
2266
2310
  }
2267
2311
  // Do nothing if already trusted.
2268
2312
 
@@ -2294,7 +2338,7 @@ export namespace NotebookActions {
2294
2338
  return showDialog({
2295
2339
  body: trans.__('Notebook is already trusted'),
2296
2340
  buttons: [Dialog.okButton()]
2297
- }).then(() => undefined);
2341
+ }).then(() => ({ trusted: true }));
2298
2342
  }
2299
2343
 
2300
2344
  return showDialog({
@@ -2314,7 +2358,9 @@ export namespace NotebookActions {
2314
2358
  cell.trusted = true;
2315
2359
  }
2316
2360
  }
2361
+ return { trusted: true };
2317
2362
  }
2363
+ return { trusted: false };
2318
2364
  });
2319
2365
  }
2320
2366
 
@@ -2412,6 +2458,7 @@ namespace Private {
2412
2458
  /**
2413
2459
  * Notebook cell executor
2414
2460
  */
2461
+ // eslint-disable-next-line no-unassigned-vars
2415
2462
  export let executor: INotebookCellExecutor;
2416
2463
 
2417
2464
  /**
@@ -2812,8 +2859,12 @@ namespace Private {
2812
2859
  export function changeCellType(
2813
2860
  notebook: Notebook,
2814
2861
  value: nbformat.CellType,
2815
- translator?: ITranslator
2862
+ options?: {
2863
+ translator?: ITranslator;
2864
+ headingLevel?: number;
2865
+ }
2816
2866
  ): void {
2867
+ const { translator, headingLevel } = options ?? {};
2817
2868
  const notebookSharedModel = notebook.model!.sharedModel;
2818
2869
  notebook.widgets.forEach((child, index) => {
2819
2870
  if (!notebook.isSelectedOrActive(child)) {
@@ -2823,8 +2874,7 @@ namespace Private {
2823
2874
  child.model.type === 'code' &&
2824
2875
  (child as CodeCell).outputArea.pendingInput
2825
2876
  ) {
2826
- translator = translator || nullTranslator;
2827
- const trans = translator.load('jupyterlab');
2877
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
2828
2878
  // Do not permit changing cell type when input is pending
2829
2879
  void showDialog({
2830
2880
  title: trans.__('Cell type not changed due to pending input'),
@@ -2836,8 +2886,7 @@ namespace Private {
2836
2886
  return;
2837
2887
  }
2838
2888
  if (child.model.getMetadata('editable') == false) {
2839
- translator = translator || nullTranslator;
2840
- const trans = translator.load('jupyterlab');
2889
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
2841
2890
  // Do not permit changing cell type when the cell is readonly
2842
2891
  void showDialog({
2843
2892
  title: trans.__('Cell is read-only'),
@@ -2848,6 +2897,10 @@ namespace Private {
2848
2897
  }
2849
2898
  if (child.model.type !== value) {
2850
2899
  const raw = child.model.toJSON();
2900
+ let newSource = raw.source as string;
2901
+ if (headingLevel !== undefined) {
2902
+ newSource = Private.setMarkdownHeader(newSource, headingLevel);
2903
+ }
2851
2904
  notebookSharedModel.transact(() => {
2852
2905
  notebookSharedModel.deleteCell(index);
2853
2906
  if (value === 'code') {
@@ -2861,7 +2914,7 @@ namespace Private {
2861
2914
  const newCell = notebookSharedModel.insertCell(index, {
2862
2915
  id: raw.id,
2863
2916
  cell_type: value,
2864
- source: raw.source,
2917
+ source: newSource,
2865
2918
  metadata: raw.metadata
2866
2919
  });
2867
2920
  if (raw.attachments && ['markdown', 'raw'].includes(value)) {
@@ -2869,6 +2922,15 @@ namespace Private {
2869
2922
  raw.attachments as nbformat.IAttachments;
2870
2923
  }
2871
2924
  });
2925
+ } else if (value === 'markdown' && headingLevel !== undefined) {
2926
+ notebookSharedModel.transact(() => {
2927
+ child.model.sharedModel.setSource(
2928
+ Private.setMarkdownHeader(
2929
+ child.model.sharedModel.getSource(),
2930
+ headingLevel
2931
+ )
2932
+ );
2933
+ });
2872
2934
  }
2873
2935
  if (value === 'markdown') {
2874
2936
  // Fetch the new widget and unrender it.
@@ -2947,11 +3009,10 @@ namespace Private {
2947
3009
  }
2948
3010
 
2949
3011
  /**
2950
- * Set the markdown header level of a cell.
3012
+ * Set the markdown header level of a cell source.
2951
3013
  */
2952
- export function setMarkdownHeader(cell: ICellModel, level: number): void {
3014
+ export function setMarkdownHeader(source: string, level: number): string {
2953
3015
  // Remove existing header or leading white space.
2954
- let source = cell.sharedModel.getSource();
2955
3016
  const regex = /^(#+\s*)|^(\s*)/;
2956
3017
  const newHeader = Array(level + 1).join('#') + ' ';
2957
3018
  const matches = regex.exec(source);
@@ -2959,7 +3020,7 @@ namespace Private {
2959
3020
  if (matches) {
2960
3021
  source = source.slice(matches[0].length);
2961
3022
  }
2962
- cell.sharedModel.setSource(newHeader + source);
3023
+ return newHeader + source;
2963
3024
  }
2964
3025
 
2965
3026
  /** Functionality related to collapsible headings */