Skip to content

Commit b6253d9

Browse files
authored
Fix creating files in custom drives, fix ContentsManagerMock (jupyterlab#15291)
* Fix create files in custom drives * fix creating new notebooks * update method name * fix handling of localpath * fix handling of drives in ContentsManagerMock * fix handling of drives in ContentsManagerMock * Add test for creating new files in drives * fix typo * Fix delete * Fix restoreCheckpoints
1 parent 7a7df78 commit b6253d9

File tree

4 files changed

+166
-35
lines changed

4 files changed

+166
-35
lines changed

packages/filebrowser/src/browser.ts

+23
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Distributed under the terms of the Modified BSD License.
33

44
import { showErrorMessage } from '@jupyterlab/apputils';
5+
import { PathExt } from '@jupyterlab/coreutils';
56
import { IDocumentManager } from '@jupyterlab/docmanager';
67
import { Contents, ServerConnection } from '@jupyterlab/services';
78
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
@@ -252,6 +253,11 @@ export class FileBrowser extends SidePanel {
252253
private async _createNew(
253254
options: Contents.ICreateOptions
254255
): Promise<Contents.IModel> {
256+
// normalize the path if the file is created from a custom drive
257+
if (options.path) {
258+
const localPath = this._manager.services.contents.localPath(options.path);
259+
options.path = this._toDrivePath(this.model.driveName, localPath);
260+
}
255261
try {
256262
const model = await this._manager.newUntitled(options);
257263
await this.listing.selectItemByName(model.name, true);
@@ -403,6 +409,23 @@ export class FileBrowser extends SidePanel {
403409
}
404410
}
405411

412+
/**
413+
* Given a drive name and a local path, return the full
414+
* drive path which includes the drive name and the local path.
415+
*
416+
* @param driveName: the name of the drive
417+
* @param localPath: the local path on the drive.
418+
*
419+
* @returns the full drive path
420+
*/
421+
private _toDrivePath(driveName: string, localPath: string): string {
422+
if (driveName === '') {
423+
return localPath;
424+
} else {
425+
return `${driveName}:${PathExt.removeSlash(localPath)}`;
426+
}
427+
}
428+
406429
protected listing: DirListing;
407430
protected crumbs: BreadCrumbs;
408431
protected mainPanel: Panel;

packages/filebrowser/test/browser.spec.ts

+65
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager';
1010
import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry';
1111
import { ServiceManager } from '@jupyterlab/services';
1212
import { signalToPromise } from '@jupyterlab/testing';
13+
import { Drive } from '@jupyterlab/services';
1314
import { ServiceManagerMock } from '@jupyterlab/services/lib/testutils';
1415
import { DocumentWidgetOpenerMock } from '@jupyterlab/docregistry/lib/testutils';
1516
import { simulate } from 'simulate-event';
@@ -130,3 +131,67 @@ describe('filebrowser/browser', () => {
130131
});
131132
});
132133
});
134+
135+
describe('FileBrowser with Drives', () => {
136+
const DRIVE_NAME = 'TestDrive';
137+
let fileBrowser: TestFileBrowser;
138+
let manager: IDocumentManager;
139+
let serviceManager: ServiceManager.IManager;
140+
let registry: DocumentRegistry;
141+
let model: FilterFileBrowserModel;
142+
143+
beforeAll(async () => {
144+
const opener = new DocumentWidgetOpenerMock();
145+
146+
registry = new DocumentRegistry({
147+
textModelFactory: new TextModelFactory()
148+
});
149+
serviceManager = new ServiceManagerMock();
150+
manager = new DocumentManager({
151+
registry,
152+
opener,
153+
manager: serviceManager
154+
});
155+
156+
const drive = new Drive({
157+
name: DRIVE_NAME,
158+
serverSettings: serviceManager.serverSettings
159+
});
160+
serviceManager.contents.addDrive(drive);
161+
model = new FilterFileBrowserModel({ manager, driveName: drive.name });
162+
});
163+
164+
beforeEach(() => {
165+
const options: FileBrowser.IOptions = {
166+
model,
167+
id: ''
168+
};
169+
fileBrowser = new TestFileBrowser(options);
170+
Widget.attach(fileBrowser, document.body);
171+
});
172+
173+
describe('#createNewFile', () => {
174+
it('should create the file in the drive', async () => {
175+
const created = fileBrowser.createNewFile({ ext: '.txt' });
176+
await signalToPromise(fileBrowser.renameCalled);
177+
const editNode = document.querySelector(`.${EDITOR_CLASS}`);
178+
if (!editNode) {
179+
throw new Error('Edit node not found');
180+
}
181+
const itemNode = Array.from(
182+
document.querySelectorAll(`.${ITEM_CLASS}`)
183+
).find(el => {
184+
return el.contains(editNode);
185+
});
186+
if (!itemNode) {
187+
throw new Error('Item node not found');
188+
}
189+
simulate(editNode, 'keydown', {
190+
keyCode: 13,
191+
key: 'Enter'
192+
});
193+
const fileModel = await created;
194+
expect(fileModel.path).toContain(DRIVE_NAME);
195+
});
196+
});
197+
});

packages/notebook-extension/src/index.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ import { IDocumentManager } from '@jupyterlab/docmanager';
4848
import { ToolbarItems as DocToolbarItems } from '@jupyterlab/docmanager-extension';
4949
import { DocumentRegistry, IDocumentWidget } from '@jupyterlab/docregistry';
5050
import { ISearchProviderRegistry } from '@jupyterlab/documentsearch';
51-
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
51+
import {
52+
IDefaultFileBrowser,
53+
IFileBrowserFactory
54+
} from '@jupyterlab/filebrowser';
5255
import { ILauncher } from '@jupyterlab/launcher';
5356
import {
5457
ILSPCodeExtractorsManager,
@@ -363,7 +366,8 @@ const trackerPlugin: JupyterFrontEndPlugin<INotebookTracker> = {
363366
ISettingRegistry,
364367
ISessionContextDialogs,
365368
ITranslator,
366-
IFormRendererRegistry
369+
IFormRendererRegistry,
370+
IFileBrowserFactory
367371
],
368372
activate: activateNotebookHandler,
369373
autoStart: true
@@ -1594,7 +1598,8 @@ function activateNotebookHandler(
15941598
settingRegistry: ISettingRegistry | null,
15951599
sessionDialogs_: ISessionContextDialogs | null,
15961600
translator_: ITranslator | null,
1597-
formRegistry: IFormRendererRegistry | null
1601+
formRegistry: IFormRendererRegistry | null,
1602+
filebrowserFactory: IFileBrowserFactory | null
15981603
): INotebookTracker {
15991604
const translator = translator_ ?? nullTranslator;
16001605
const sessionDialogs =
@@ -1957,7 +1962,9 @@ function activateNotebookHandler(
19571962
caption: trans.__('Create a new notebook'),
19581963
icon: args => (args['isPalette'] ? undefined : notebookIcon),
19591964
execute: args => {
1960-
const cwd = (args['cwd'] as string) || (defaultBrowser?.model.path ?? '');
1965+
const currentBrowser =
1966+
filebrowserFactory?.tracker.currentWidget ?? defaultBrowser;
1967+
const cwd = (args['cwd'] as string) || (currentBrowser?.model.path ?? '');
19611968
const kernelId = (args['kernelId'] as string) || '';
19621969
const kernelName = (args['kernelName'] as string) || '';
19631970
return createNew(cwd, kernelId, kernelName);

packages/services/src/testutils.ts

+67-31
Original file line numberDiff line numberDiff line change
@@ -307,31 +307,48 @@ export const SessionConnectionMock = jest.fn<
307307
* A mock contents manager.
308308
*/
309309
export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
310-
const files = new Map<string, Contents.IModel>();
310+
const files = new Map<string, Map<string, Contents.IModel>>();
311311
const dummy = new ContentsManager();
312312
const checkpoints = new Map<string, Contents.ICheckpointModel>();
313313
const checkPointContent = new Map<string, string>();
314314

315315
const baseModel = Private.createFile({ type: 'directory' });
316-
files.set('', { ...baseModel, path: '', name: '' });
316+
// create the default drive
317+
files.set(
318+
'',
319+
new Map<string, Contents.IModel>([
320+
['', { ...baseModel, path: '', name: '' }]
321+
])
322+
);
317323

318324
const thisObject: Contents.IManager = {
319325
...jest.requireActual('@jupyterlab/services'),
320326
newUntitled: jest.fn(options => {
321-
const model = Private.createFile(options || {});
322-
files.set(model.path, model);
327+
const driveName = dummy.driveName(options?.path || '');
328+
const localPath = dummy.localPath(options?.path || '');
329+
// create the test file without the drive name
330+
const createOptions = { ...options, path: localPath };
331+
const model = Private.createFile(createOptions || {});
332+
// re-add the drive name to the model
333+
const drivePath = driveName ? `${driveName}:${model.path}` : model.path;
334+
const driveModel = {
335+
...model,
336+
path: drivePath
337+
};
338+
files.get(driveName)!.set(model.path, driveModel);
323339
fileChangedSignal.emit({
324340
type: 'new',
325341
oldValue: null,
326-
newValue: model
342+
newValue: driveModel
327343
});
328-
return Promise.resolve(model);
344+
return Promise.resolve(driveModel);
329345
}),
330346
createCheckpoint: jest.fn(path => {
331347
const lastModified = new Date().toISOString();
332348
const data = { id: UUID.uuid4(), last_modified: lastModified };
333349
checkpoints.set(path, data);
334-
checkPointContent.set(path, files.get(path)?.content);
350+
// TODO: handle drives
351+
checkPointContent.set(path, files.get('')!.get(path)?.content);
335352
return Promise.resolve(data);
336353
}),
337354
listCheckpoints: jest.fn(path => {
@@ -352,7 +369,8 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
352369
if (!checkpoints.has(path)) {
353370
return Private.makeResponseError(404);
354371
}
355-
(files.get(path) as any).content = checkPointContent.get(path);
372+
// TODO: handle drives
373+
(files.get('')!.get(path) as any).content = checkPointContent.get(path);
356374
return Promise.resolve();
357375
}),
358376
getSharedModelFactory: jest.fn(() => {
@@ -368,11 +386,14 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
368386
return dummy.resolvePath(root, path);
369387
}),
370388
get: jest.fn((path, options) => {
371-
path = Private.fixSlash(path);
372-
if (!files.has(path)) {
389+
const driveName = dummy.driveName(path);
390+
const localPath = dummy.localPath(path);
391+
const drive = files.get(driveName)!;
392+
path = Private.fixSlash(localPath);
393+
if (!drive.has(path)) {
373394
return Private.makeResponseError(404);
374395
}
375-
const model = files.get(path)!;
396+
const model = drive.get(path)!;
376397
const overrides: { hash?: string; last_modified?: string } = {};
377398
if (path == 'random-hash.txt') {
378399
overrides.hash = Math.random().toString();
@@ -385,10 +406,11 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
385406
if (model.type === 'directory') {
386407
if (options?.content !== false) {
387408
const content: Contents.IModel[] = [];
388-
files.forEach(fileModel => {
409+
drive.forEach(fileModel => {
410+
const localPath = dummy.localPath(fileModel.path);
389411
if (
390412
// If file path is under this directory, add it to contents array.
391-
PathExt.dirname(fileModel.path) == model.path &&
413+
PathExt.dirname(localPath) == model.path &&
392414
// But the directory should exclude itself from the contents array.
393415
fileModel !== model
394416
) {
@@ -408,16 +430,20 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
408430
return dummy.driveName(path);
409431
}),
410432
rename: jest.fn((oldPath, newPath) => {
411-
oldPath = Private.fixSlash(oldPath);
412-
newPath = Private.fixSlash(newPath);
413-
if (!files.has(oldPath)) {
433+
const driveName = dummy.driveName(oldPath);
434+
const drive = files.get(driveName)!;
435+
let oldLocalPath = dummy.localPath(oldPath);
436+
let newLocalPath = dummy.localPath(newPath);
437+
oldLocalPath = Private.fixSlash(oldLocalPath);
438+
newLocalPath = Private.fixSlash(newLocalPath);
439+
if (!drive.has(oldLocalPath)) {
414440
return Private.makeResponseError(404);
415441
}
416-
const oldValue = files.get(oldPath)!;
417-
files.delete(oldPath);
418-
const name = PathExt.basename(newPath);
419-
const newValue = { ...oldValue, name, path: newPath };
420-
files.set(newPath, newValue);
442+
const oldValue = drive.get(oldPath)!;
443+
drive.delete(oldPath);
444+
const name = PathExt.basename(newLocalPath);
445+
const newValue = { ...oldValue, name, path: newLocalPath };
446+
drive.set(newPath, newValue);
421447
fileChangedSignal.emit({
422448
type: 'rename',
423449
oldValue,
@@ -426,12 +452,15 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
426452
return Promise.resolve(newValue);
427453
}),
428454
delete: jest.fn(path => {
429-
path = Private.fixSlash(path);
430-
if (!files.has(path)) {
455+
const driveName = dummy.driveName(path);
456+
const localPath = dummy.localPath(path);
457+
const drive = files.get(driveName)!;
458+
path = Private.fixSlash(localPath);
459+
if (!drive.has(path)) {
431460
return Private.makeResponseError(404);
432461
}
433-
const oldValue = files.get(path)!;
434-
files.delete(path);
462+
const oldValue = drive.get(path)!;
463+
drive.delete(path);
435464
fileChangedSignal.emit({
436465
type: 'delete',
437466
oldValue,
@@ -445,21 +474,22 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
445474
}
446475
path = Private.fixSlash(path);
447476
const timeStamp = new Date().toISOString();
448-
if (files.has(path)) {
477+
const drive = files.get(dummy.driveName(path))!;
478+
if (drive.has(path)) {
449479
const updates =
450480
path == 'frozen-time-and-hash.txt'
451481
? {}
452482
: {
453483
last_modified: timeStamp,
454484
hash: timeStamp
455485
};
456-
files.set(path, {
457-
...files.get(path)!,
486+
drive.set(path, {
487+
...drive.get(path)!,
458488
...options,
459489
...updates
460490
});
461491
} else {
462-
files.set(path, {
492+
drive.set(path, {
463493
path,
464494
name: PathExt.basename(path),
465495
content: '',
@@ -477,15 +507,21 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
477507
fileChangedSignal.emit({
478508
type: 'save',
479509
oldValue: null,
480-
newValue: files.get(path)!
510+
newValue: drive.get(path)!
481511
});
482-
return Promise.resolve(files.get(path)!);
512+
return Promise.resolve(drive.get(path)!);
483513
}),
484514
getDownloadUrl: jest.fn(path => {
485515
return dummy.getDownloadUrl(path);
486516
}),
487517
addDrive: jest.fn(drive => {
488518
dummy.addDrive(drive);
519+
files.set(
520+
drive.name,
521+
new Map<string, Contents.IModel>([
522+
['', { ...baseModel, path: '', name: '' }]
523+
])
524+
);
489525
}),
490526
dispose: jest.fn()
491527
};

0 commit comments

Comments
 (0)