@jlab-enhanced/favorites 3.3.0 → 3.4.0

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/lib/components.js CHANGED
@@ -2,6 +2,7 @@ import { folderIcon, LabIcon, fileIcon, ReactWidget, UseSignal } from '@jupyterl
2
2
  import { Signal } from '@lumino/signaling';
3
3
  import * as React from 'react';
4
4
  import { getFavoritesIcon, getName, getPinnerActionDescription, mergePaths } from './utils';
5
+ import { Drag } from '@lumino/dragdrop';
5
6
  /**
6
7
  * The parent node class for Favorites content.
7
8
  */
@@ -38,6 +39,10 @@ const FILEBROWSER_HEADER_CLASS = 'jp-FileBrowser-header';
38
39
  * This icon is overlaid on top of the FileBrowser content via CSS.
39
40
  */
40
41
  const FAVORITE_BREADCRUMB_ICON_CLASS = 'jp-Favorites-BreadCrumbs-Icon';
42
+ /**
43
+ * The spacing from the bottom of the FileBrowser to leave when resizing the Favorites container.
44
+ */
45
+ const BOTTOM_SPACING = 100;
41
46
  const FavoriteComponent = (props) => {
42
47
  const { favorite, handleClick } = props;
43
48
  let [displayName, dirname] = getName(favorite.path);
@@ -85,6 +90,62 @@ export const FavoritesBreadCrumbs = (props) => {
85
90
  React.createElement(icon.react, { className: FAVORITE_BREADCRUMB_ICON_CLASS, tag: "span" })));
86
91
  }));
87
92
  };
93
+ function FavoritesContainer({ visibleFavorites, manager }) {
94
+ const containerRef = React.useRef(null);
95
+ const [isResizing, setIsResizing] = React.useState(false);
96
+ const cursorDisposableRef = React.useRef(null);
97
+ const handleMouseDown = () => {
98
+ setIsResizing(true);
99
+ cursorDisposableRef.current = Drag.overrideCursor('ns-resize');
100
+ };
101
+ const handleMouseMove = React.useCallback((e) => {
102
+ if (!isResizing) {
103
+ return;
104
+ }
105
+ const container = containerRef.current;
106
+ if (!container) {
107
+ return;
108
+ }
109
+ // Height of filebrowser widget
110
+ const parentElement = container.closest('.jp-FileBrowser');
111
+ const parentRect = parentElement === null || parentElement === void 0 ? void 0 : parentElement.getBoundingClientRect();
112
+ if (!parentRect) {
113
+ return;
114
+ }
115
+ const rect = container.getBoundingClientRect();
116
+ const newHeight = e.clientY - rect.top;
117
+ const maxHeight = parentRect.height - BOTTOM_SPACING;
118
+ if (newHeight > 24 && newHeight < maxHeight) {
119
+ container.style.maxHeight = maxHeight + 'px'; // To ensure default max-height of css is overridden
120
+ container.style.height = newHeight + 'px';
121
+ }
122
+ }, [isResizing]);
123
+ const handleMouseUp = React.useCallback(() => {
124
+ var _a;
125
+ setIsResizing(false);
126
+ (_a = cursorDisposableRef.current) === null || _a === void 0 ? void 0 : _a.dispose();
127
+ cursorDisposableRef.current = null;
128
+ }, []);
129
+ React.useEffect(() => {
130
+ if (isResizing) {
131
+ document.addEventListener('mousemove', handleMouseMove);
132
+ document.addEventListener('mouseup', handleMouseUp);
133
+ return () => {
134
+ document.removeEventListener('mousemove', handleMouseMove);
135
+ document.removeEventListener('mouseup', handleMouseUp);
136
+ };
137
+ }
138
+ }, [isResizing, handleMouseMove, handleMouseUp]);
139
+ React.useEffect(() => {
140
+ return () => {
141
+ var _a;
142
+ (_a = cursorDisposableRef.current) === null || _a === void 0 ? void 0 : _a.dispose();
143
+ };
144
+ }, []);
145
+ return (React.createElement(React.Fragment, null,
146
+ React.createElement("div", { ref: containerRef, className: FAVORITE_CONTAINER_CLASS }, (visibleFavorites !== null && visibleFavorites !== void 0 ? visibleFavorites : []).map(f => (React.createElement(FavoriteComponent, { key: `favorites-item-${f.path}`, favorite: f, handleClick: manager.handleClick.bind(manager) })))),
147
+ React.createElement("div", { className: "jp-Favorites-resize-handle", onMouseDown: handleMouseDown })));
148
+ }
88
149
  export class FavoritesWidget extends ReactWidget {
89
150
  constructor(manager, filebrowser) {
90
151
  super();
@@ -101,7 +162,7 @@ export class FavoritesWidget extends ReactWidget {
101
162
  return (React.createElement(UseSignal, { signal: this.manager.favoritesChanged, initialSender: this.manager, initialArgs: this.manager.visibleFavorites() }, (manager, visibleFavorites) => (React.createElement("div", null,
102
163
  React.createElement(UseSignal, { signal: manager.visibilityChanged, initialSender: manager, initialArgs: manager.isVisible() }, (manager, isVisible) => isVisible && (React.createElement(React.Fragment, null,
103
164
  React.createElement("div", { className: FAVORITE_HEADER_CLASS }, "Favorites"),
104
- React.createElement("div", { className: FAVORITE_CONTAINER_CLASS }, (visibleFavorites !== null && visibleFavorites !== void 0 ? visibleFavorites : []).map(f => (React.createElement(FavoriteComponent, { key: `favorites-item-${f.path}`, favorite: f, handleClick: manager.handleClick.bind(manager) })))),
165
+ React.createElement(FavoritesContainer, { visibleFavorites: visibleFavorites, manager: manager }),
105
166
  React.createElement("div", { className: FILEBROWSER_HEADER_CLASS }, "File Browser"))))))));
106
167
  }
107
168
  }
package/lib/index.d.ts CHANGED
@@ -6,5 +6,5 @@ export { IFavorites, FAVORITE_TAG } from './token';
6
6
  * Plugin that provides the custom notebook factory with star icons
7
7
  */
8
8
  export declare const notebookFactoryPlugin: JupyterFrontEndPlugin<NotebookPanel.IContentFactory>;
9
- declare const _default: (JupyterFrontEndPlugin<IFavorites> | JupyterFrontEndPlugin<NotebookPanel.IContentFactory>)[];
9
+ declare const _default: (JupyterFrontEndPlugin<NotebookPanel.IContentFactory> | JupyterFrontEndPlugin<IFavorites>)[];
10
10
  export default _default;
package/lib/index.js CHANGED
@@ -253,7 +253,7 @@ const favorites = {
253
253
  okLabel: 'Rename',
254
254
  placeholder: 'Display name'
255
255
  });
256
- displayName = result.button.accept ? (_a = result.value) !== null && _a !== void 0 ? _a : '' : '';
256
+ displayName = result.button.accept ? ((_a = result.value) !== null && _a !== void 0 ? _a : '') : '';
257
257
  }
258
258
  if (!displayName) {
259
259
  return;
@@ -360,11 +360,28 @@ export const notebookFactoryPlugin = {
360
360
  provides: NotebookPanel.IContentFactory,
361
361
  requires: [IEditorServices],
362
362
  autoStart: true,
363
- activate: (app, editorServices) => {
363
+ activate: async (app, editorServices) => {
364
+ var _a, _b;
365
+ let mystFactory;
366
+ if (app.hasPlugin('jupyterlab-myst:content-factory')) {
367
+ const mystPlugins = (await import('jupyterlab-myst')).default;
368
+ const mystPlugin = mystPlugins.filter(plugin => plugin.provides === NotebookPanel.IContentFactory)[0];
369
+ if (mystPlugin) {
370
+ const dependencies = await Promise.all([
371
+ ...((_a = mystPlugin.requires) !== null && _a !== void 0 ? _a : []).map(token => app.resolveRequiredService(token)),
372
+ ...((_b = mystPlugin.optional) !== null && _b !== void 0 ? _b : []).map(token => app.resolveOptionalService(token))
373
+ ]);
374
+ mystFactory = (await mystPlugin.activate(app, ...dependencies));
375
+ }
376
+ else {
377
+ console.error('jupyterlab-favorites found jupyterlab-myst:content-factory plugin, but could not activate content factory for compatibility fix');
378
+ }
379
+ }
364
380
  const editorFactory = editorServices.factoryService.newInlineEditor;
365
381
  const factory = new StarredNotebookContentFactory({
366
382
  editorFactory,
367
- app
383
+ app,
384
+ mystFactory
368
385
  });
369
386
  return factory;
370
387
  }
package/lib/manager.d.ts CHANGED
@@ -40,4 +40,6 @@ export declare class FavoritesManager {
40
40
  private _favorites;
41
41
  private _serverRoot;
42
42
  private _showWidget;
43
+ private _sortOrder;
44
+ private _groupByType;
43
45
  }
package/lib/manager.js CHANGED
@@ -8,6 +8,8 @@ export class FavoritesManager {
8
8
  this.visibilityChanged = new Signal(this);
9
9
  this._favorites = [];
10
10
  this._showWidget = false;
11
+ this._sortOrder = 'name';
12
+ this._groupByType = true;
11
13
  this._showWidget = true;
12
14
  this._serverRoot = serverRoot;
13
15
  this._commandRegistry = commands;
@@ -124,15 +126,24 @@ export class FavoritesManager {
124
126
  }
125
127
  visibleFavorites(sort = true) {
126
128
  const filtered = this.favorites.filter(f => !f.hidden);
127
- if (!sort) {
129
+ if (!sort || this._sortOrder === 'unsorted') {
128
130
  return filtered;
129
131
  }
130
132
  return filtered.sort((a, b) => {
131
- if (a.contentType === b.contentType) {
132
- return getName(a.path) <= getName(b.path) ? -1 : 1;
133
+ // Group by content type first if enabled
134
+ if (this._groupByType && a.contentType !== b.contentType) {
135
+ return a.contentType < b.contentType ? -1 : 1;
136
+ }
137
+ // Sort by selected criterion using locale-aware comparison
138
+ if (this._sortOrder === 'name') {
139
+ // Use custom display name if set, otherwise fall back to basename
140
+ const nameA = a.name || getName(a.path)[0];
141
+ const nameB = b.name || getName(b.path)[0];
142
+ return nameA.localeCompare(nameB);
133
143
  }
134
144
  else {
135
- return a.contentType < b.contentType ? -1 : 1;
145
+ // sortOrder === 'path'
146
+ return a.path.localeCompare(b.path);
136
147
  }
137
148
  });
138
149
  }
@@ -148,9 +159,14 @@ export class FavoritesManager {
148
159
  this.overwriteSettings({ favorites: defaultFavorites });
149
160
  }
150
161
  async loadFavorites() {
151
- var _a;
162
+ var _a, _b, _c;
152
163
  const setting = await this._settingsRegistry.get(SettingIDs.favorites, 'favorites');
153
164
  const favorites = ((_a = setting.composite) !== null && _a !== void 0 ? _a : []);
165
+ // Load sort settings
166
+ const sortOrderSetting = await this._settingsRegistry.get(SettingIDs.favorites, 'sortOrder');
167
+ this._sortOrder = ((_b = sortOrderSetting.composite) !== null && _b !== void 0 ? _b : 'name');
168
+ const groupByTypeSetting = await this._settingsRegistry.get(SettingIDs.favorites, 'groupByType');
169
+ this._groupByType = ((_c = groupByTypeSetting.composite) !== null && _c !== void 0 ? _c : true);
154
170
  this.favorites = favorites.map(favorite => {
155
171
  var _a;
156
172
  return { ...favorite, root: (_a = favorite.root) !== null && _a !== void 0 ? _a : this.serverRoot };
@@ -1,7 +1,7 @@
1
1
  import { JupyterFrontEnd } from '@jupyterlab/application';
2
2
  import { Widget } from '@lumino/widgets';
3
- import { Cell, IInputPrompt } from '@jupyterlab/cells';
4
- import { NotebookPanel } from '@jupyterlab/notebook';
3
+ import { Cell, IInputPrompt, MarkdownCell } from '@jupyterlab/cells';
4
+ import { Notebook, NotebookPanel } from '@jupyterlab/notebook';
5
5
  export declare class StarredInputPrompt extends Widget implements IInputPrompt {
6
6
  private _app;
7
7
  private _executionCount;
@@ -15,10 +15,14 @@ export declare class StarredInputPrompt extends Widget implements IInputPrompt {
15
15
  export declare namespace StarredNotebookContentFactory {
16
16
  interface IOptions extends Cell.ContentFactory.IOptions {
17
17
  app: JupyterFrontEnd;
18
+ mystFactory?: NotebookPanel.IContentFactory;
18
19
  }
19
20
  }
20
21
  export declare class StarredNotebookContentFactory extends NotebookPanel.ContentFactory {
21
- private _app;
22
22
  constructor(options: StarredNotebookContentFactory.IOptions);
23
23
  createInputPrompt(): StarredInputPrompt;
24
+ createMarkdownCell(options: MarkdownCell.IOptions): MarkdownCell;
25
+ createNotebook(options: Notebook.IOptions): Notebook;
26
+ private _app;
27
+ private _mystFactory?;
24
28
  }
package/lib/starPrompt.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { BoxLayout, Widget } from '@lumino/widgets';
2
- import { CommandIDs } from './token';
3
- import { InputPrompt } from '@jupyterlab/cells';
2
+ import { CommandIDs, FAVORITE_FILTER_CLASS, FAVORITE_TAG } from './token';
3
+ import { InputPrompt, MarkdownCell } from '@jupyterlab/cells';
4
4
  import { ToolbarButton } from '@jupyterlab/ui-components';
5
5
  import { filledStarIcon, starIcon } from './icons';
6
- import { NotebookPanel } from '@jupyterlab/notebook';
6
+ import { Notebook, NotebookPanel } from '@jupyterlab/notebook';
7
7
  const INPUT_PROMPT_CLASS = 'jp-InputPrompt';
8
8
  const INPUT_PROMPT_NUMBER_CLASS = 'jp-Favorites-InputPromptNumber';
9
9
  const FAVORITE_ICON_ON_CLASS = 'jp-Favorites-star-class';
@@ -50,12 +50,100 @@ export class StarredInputPrompt extends Widget {
50
50
  this._promptIndicator.executionCount = value;
51
51
  }
52
52
  }
53
+ class FavoritesNotebook extends Notebook {
54
+ constructor(options) {
55
+ super(options);
56
+ }
57
+ get activeCellIndex() {
58
+ if (!this.model) {
59
+ return -1;
60
+ }
61
+ return this.widgets.length ? super.activeCellIndex : -1;
62
+ }
63
+ set activeCellIndex(newValue) {
64
+ const oldValue = super.activeCellIndex;
65
+ // Validate bounds
66
+ if (!this.model || !this.widgets.length) {
67
+ newValue = -1;
68
+ }
69
+ else {
70
+ newValue = Math.max(newValue, 0);
71
+ newValue = Math.min(newValue, this.widgets.length - 1);
72
+ }
73
+ // If favorites filter is not active, use default behavior
74
+ if (!this._isFavoritesFilterActive()) {
75
+ super.activeCellIndex = newValue;
76
+ return;
77
+ }
78
+ // If target cell is favorite, use default behavior
79
+ if (this._isCellFavorite(newValue)) {
80
+ super.activeCellIndex = newValue;
81
+ return;
82
+ }
83
+ // Target cell is not a favorite, find nearest favorite
84
+ const direction = newValue > oldValue ? 1 : -1;
85
+ const nearestFavoriteIndex = this._findNearestFavoriteCell(newValue, direction);
86
+ if (nearestFavoriteIndex !== -1) {
87
+ super.activeCellIndex = nearestFavoriteIndex;
88
+ return;
89
+ }
90
+ else {
91
+ // At the edges, stick the nearest favourite cell in any direction;
92
+ // this also helps when the favorite cell is moved around as it will
93
+ // snap back to it, rather than leaving the index on a now hidden cell.
94
+ const alternative = this._findNearestFavoriteCell(newValue, direction > 0 ? -1 : 1);
95
+ super.activeCellIndex = alternative;
96
+ }
97
+ }
98
+ /**
99
+ * Check if a cell at the given index is marked as favorite
100
+ */
101
+ _isCellFavorite(cellIndex) {
102
+ if (cellIndex < 0 || cellIndex >= this.widgets.length) {
103
+ return false;
104
+ }
105
+ const cell = this.widgets[cellIndex];
106
+ const tags = cell.model.getMetadata('tags');
107
+ return Array.isArray(tags) && tags.includes(FAVORITE_TAG);
108
+ }
109
+ /**
110
+ * Check if the favorites filter is currently active
111
+ */
112
+ _isFavoritesFilterActive() {
113
+ return this.node.classList.contains(FAVORITE_FILTER_CLASS);
114
+ }
115
+ /**
116
+ * Find the nearest favorite cell in a given direction
117
+ * @param startIndex - Index to start searching from
118
+ * @param direction - 1 for down, -1 for up
119
+ */
120
+ _findNearestFavoriteCell(startIndex, direction) {
121
+ let currentIndex = startIndex + direction;
122
+ while (currentIndex >= 0 && currentIndex < this.widgets.length) {
123
+ if (this._isCellFavorite(currentIndex)) {
124
+ return currentIndex;
125
+ }
126
+ currentIndex += direction;
127
+ }
128
+ return -1;
129
+ }
130
+ }
53
131
  export class StarredNotebookContentFactory extends NotebookPanel.ContentFactory {
54
132
  constructor(options) {
55
133
  super(options);
56
134
  this._app = options.app;
135
+ this._mystFactory = options.mystFactory;
57
136
  }
58
137
  createInputPrompt() {
59
138
  return new StarredInputPrompt(this._app);
60
139
  }
140
+ createMarkdownCell(options) {
141
+ if (this._mystFactory) {
142
+ return this._mystFactory.createMarkdownCell(options);
143
+ }
144
+ return new MarkdownCell(options).initializeState();
145
+ }
146
+ createNotebook(options) {
147
+ return new FavoritesNotebook(options);
148
+ }
61
149
  }
package/lib/token.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare namespace PluginIDs {
4
4
  const notebookFactory = "favorites-notebook-factory";
5
5
  }
6
6
  export type ShowStarsTypes = 'allCells' | 'onlyFavoriteCells' | 'never';
7
+ export type SortOrder = 'unsorted' | 'name' | 'path';
7
8
  export declare namespace CommandIDs {
8
9
  const addOrRemoveFavorite: string;
9
10
  const removeFavorite: string;
@@ -33,6 +34,8 @@ export declare namespace IFavorites {
33
34
  type FavoritesSettings = {
34
35
  favorites?: Array<IFavorites.Favorite>;
35
36
  showWidget?: boolean;
37
+ sortOrder?: SortOrder;
38
+ groupByType?: boolean;
36
39
  };
37
40
  }
38
41
  export declare const IFavorites: Token<IFavorites>;
@@ -43,3 +46,7 @@ export interface IFavorites {
43
46
  * Cell tag used to mark cell as favorite
44
47
  */
45
48
  export declare const FAVORITE_TAG = "favorite";
49
+ /**
50
+ * Class set to notebook when filtering cells by favorite is enabled
51
+ */
52
+ export declare const FAVORITE_FILTER_CLASS = "jp-favorites-filter-active";
package/lib/token.js CHANGED
@@ -27,3 +27,7 @@ export const IFavorites = new Token('jupyterlab-favorites:IFavorites');
27
27
  * Cell tag used to mark cell as favorite
28
28
  */
29
29
  export const FAVORITE_TAG = 'favorite';
30
+ /**
31
+ * Class set to notebook when filtering cells by favorite is enabled
32
+ */
33
+ export const FAVORITE_FILTER_CLASS = 'jp-favorites-filter-active';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jlab-enhanced/favorites",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Add the ability to save favorite folders to JupyterLab for quicker browsing",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -65,7 +65,9 @@
65
65
  },
66
66
  "resolutions": {
67
67
  "react-dom": "^18.2.0",
68
- "react": "^18.2.0"
68
+ "react": "^18.2.0",
69
+ "lib0": "0.2.111",
70
+ "mermaid": "11.12.2"
69
71
  },
70
72
  "dependencies": {
71
73
  "@jupyterlab/application": "^4.0.5",
@@ -82,11 +84,13 @@
82
84
  "@lumino/commands": "^2.0.1",
83
85
  "@lumino/coreutils": "^2.0.1",
84
86
  "@lumino/signaling": "^2.0.0",
85
- "@lumino/widgets": "^2.0.1"
87
+ "@lumino/widgets": "^2.0.1",
88
+ "jupyterlab-myst": "^2.4.0"
86
89
  },
87
90
  "devDependencies": {
88
91
  "@jupyterlab/builder": "^4.0.0",
89
92
  "@types/json-schema": "^7.0.11",
93
+ "@types/node": "^20.11.27",
90
94
  "@types/react": "^18.0.26",
91
95
  "@types/react-addons-linked-state-mixin": "^0.14.22",
92
96
  "@typescript-eslint/eslint-plugin": "^6.1.0",
@@ -111,7 +115,12 @@
111
115
  "jupyterlab": {
112
116
  "extension": true,
113
117
  "outputDir": "jupyterlab_favorites/labextension",
114
- "schemaDir": "schema"
118
+ "schemaDir": "schema",
119
+ "sharedPackages": {
120
+ "jupyterlab-myst": {
121
+ "bundled": false
122
+ }
123
+ }
115
124
  },
116
125
  "styleModule": "style/index.js",
117
126
  "prettier": {
@@ -97,6 +97,23 @@
97
97
  { "const": "onlyFavoriteCells", "title": "Only favorite Cells" },
98
98
  { "const": "never", "title": "Never" }
99
99
  ]
100
+ },
101
+ "sortOrder": {
102
+ "type": "string",
103
+ "title": "Sort Order",
104
+ "description": "How to sort favorites in the list.",
105
+ "default": "name",
106
+ "oneOf": [
107
+ { "const": "unsorted", "title": "By Creation Order" },
108
+ { "const": "name", "title": "By Display Name" },
109
+ { "const": "path", "title": "By Path" }
110
+ ]
111
+ },
112
+ "groupByType": {
113
+ "type": "boolean",
114
+ "title": "Group by Type",
115
+ "description": "Group directories before files when sorting.",
116
+ "default": true
100
117
  }
101
118
  },
102
119
  "title": "Favorites",
package/style/base.css CHANGED
@@ -45,6 +45,17 @@
45
45
  max-height: 120px;
46
46
  }
47
47
 
48
+ .jp-Favorites-resize-handle {
49
+ height: 4px;
50
+ background: transparent;
51
+ cursor: ns-resize;
52
+ user-select: none;
53
+ }
54
+
55
+ .jp-Favorites-resize-handle:hover {
56
+ background: var(--jp-border-color1);
57
+ }
58
+
48
59
  .jp-Favorites {
49
60
  flex: 0 0 auto;
50
61
  overflow: visible;