Commit d0bca741 authored by Luciano Pacheco's avatar Luciano Pacheco Committed by Commit Bot

Implement base interface and types for new navigation

Add interface FilesAppEntry which is the base interface that moving
forward the app UI will converge as base type that can be displayed on
different UI components such as: navigation tree, file list and
breadcrumbs, eventually superseding Entry type.

Add VolumeEntry which implements interface FilesAppEntry to represent a
Volume, this will allow to display Volumes on file list/Right Hand Side
(RHS).

Add EntryList which implements interface FilesAppEntry to represent a
list of entries.  This will be used to implement "My Files" which will
contain a list of VolumeEntry for the volumes: Downloads, Linux Files
(Crostini) and Play Files (ARC++).

Design doc: https://docs.google.com/document/d/1X5XSLKJd0yerL-qFhpb2z9ibUVb_W3gG_tfIV7T_Qt0

Bug: 846587, 835203
Test: Unit-test for the new types.
Cq-Include-Trybots: luci.chromium.try:closure_compilation
Change-Id: Ia2fce233338f8b8e0969b77daf4c77139852c441
Reviewed-on: https://chromium-review.googlesource.com/1086680
Commit-Queue: Luciano Pacheco <lucmult@chromium.org>
Reviewed-by: default avatarNoel Gordon <noel@chromium.org>
Reviewed-by: default avatarNaoki Fukino <fukino@chromium.org>
Reviewed-by: default avatarSasha Morrissey <sashab@chromium.org>
Cr-Commit-Position: refs/heads/master@{#572496}
parent d12baeba
...@@ -209,3 +209,8 @@ IN_PROC_BROWSER_TEST_F(FileManagerJsTest, FileListSelectionModelTest) { ...@@ -209,3 +209,8 @@ IN_PROC_BROWSER_TEST_F(FileManagerJsTest, FileListSelectionModelTest) {
RunTest(base::FilePath(FILE_PATH_LITERAL( RunTest(base::FilePath(FILE_PATH_LITERAL(
"foreground/js/ui/file_list_selection_model_unittest.html"))); "foreground/js/ui/file_list_selection_model_unittest.html")));
} }
IN_PROC_BROWSER_TEST_F(FileManagerJsTest, FilesAppEntryTypes) {
RunTest(base::FilePath(
FILE_PATH_LITERAL("common/js/files_app_entry_types_unittest.html")));
}
...@@ -10,6 +10,7 @@ js_type_check("closure_compile") { ...@@ -10,6 +10,7 @@ js_type_check("closure_compile") {
":closure_compile_externs", ":closure_compile_externs",
":error_util", ":error_util",
":file_type", ":file_type",
":files_app_entry_types",
":importer_common", ":importer_common",
":lru_cache", ":lru_cache",
":metrics", ":metrics",
...@@ -46,6 +47,9 @@ js_library("async_util") { ...@@ -46,6 +47,9 @@ js_library("async_util") {
js_library("error_util") { js_library("error_util") {
} }
js_library("files_app_entry_types") {
}
js_library("file_type") { js_library("file_type") {
} }
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Entry-like types for Files app UI.
* This file defines the interface |FilesAppEntry| and some specialized
* implementations of it.
*
* These entries are intended to behave like the browser native FileSystemEntry
* (aka Entry) and FileSystemDirectoryEntry (aka DirectoryEntry), providing an
* unified API for Files app UI components. UI components should be able to
* display any implementation of FilesAppEntry.
* The main intention of those types is to be able to provide alternative
* implementations and from other sources for "entries", as well as be able to
* extend the native "entry" types.
*
* Native Entry:
* https://developer.mozilla.org/en-US/docs/Web/API/FileSystemEntry
* Native DirectoryEntry:
* https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader
*/
/**
* FilesAppEntry represents a single Entry (file, folder or root) in the Files
* app. Previously, we used the Entry type directly, but this limits the code to
* only work with native Entry type which can't be instantiated in JS.
* For now, Entry and FilesAppEntry should be used interchangeably.
* See also FilesAppDirEntry for a folder-like interface.
*
* TODO(lucmult): Replace uses of Entry with FilesAppEntry implementations.
*
* @interface
*/
class FilesAppEntry {
constructor() {
/**
* @public {!boolean} true if this entry represents a Directory-like entry,
* as in have sub-entries and implements {createReader} method.
* This attribute is defined on Entry.
*/
this.isDirectory = false;
/**
* @public {!boolean} true if this entry represents a File-like entry.
* Implementations of FilesAppEntry are expected to have this as |true|.
* Whereas implementations of FilesAppDirEntry are expected to have this as
* |false|.
* This attribute is defined on Entry.
*/
this.isFile = true;
/**
* @public {string} absolute path from the file system's root to the entry.
* It can also be thought of as a path which is relative to the root
* directory, prepended with a "/" character.
* This attribute is defined on Entry.
*/
this.fullPath = '';
/**
* @public {string} the name of the entry (the final part of the path,
* after the last.
* This attribute is defined on Entry.
*/
this.name = '';
/**
* @public {!string} the class name for this class. It's workaround for the
* fact that an instance created on foreground page and sent to background
* page can't be checked with "instanceof".
*/
this.type_name = 'FilesAppEntry';
}
/**
* @param {function(Entry)|function(FilesAppEntry)} success callback.
* @param {function(Entry)|function(FilesAppEntry)} error callback.
* This method is defined on Entry.
*/
getParent(success, error) {}
/**
* @return {!string} used to compare entries. It should return an unique
* identifier for such entry, usually prefixed with it's root type like:
* "fake-entry://unique/path/to/entry".
* This method is defined on Entry.
*/
toURL() {}
/**
* Return metadata via |success| callback. Relevant metadata are
* "modificationTime" and "contentMimeType".
* @param {function(Object)} success callback to be called with the result
* metadata.
* @param {function(Object)} error callback to be called in case of error or
* ignored if no error happened.
*/
getMetadata(success, error) {}
/**
* Returns true if this entry object has a native representation such as Entry
* or DirectoryEntry, this means it can interact with VolumeManager.
* @return {!boolean}
*/
get isNativeType() {}
}
/**
* A reader compatible with DirectoryEntry.createReader (from Web Standards)
* that reads a static list of entries, provided at construction time.
* https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader
* It can be used by DirectoryEntry-like such as EntryList to return its
* children entries.
*/
class StaticReader {
/**
* @param {Array<Entry|FakeEntry|FilesAppEntry>} children: Array of Entry-like
* instances that will be returned/read by this reader.
*/
constructor(children) {
this.children_ = children;
}
/**
* Reads array of entries via |success| callback.
*
* @param {function(Array<Entry|FilesAppEntry>)} success: A callback that will
* be called multiple times with the entries, last call will be called with an
* empty array indicating that no more entries available.
* @param {function(Array<Entry|FilesAppEntry>)} error: A callback that's
* never called, it's here to match the signature from the Web Standards.
*/
readEntries(success, error) {
let children = this.children_;
// readEntries is suppose to return empty result when there are no more
// files to return, so we clear the children_ attribute for next call.
this.children_ = [];
// Triggers callback asynchronously.
setTimeout(children => success(children), 0, children);
}
}
/**
* Interface with minimal API shared among different types of FilesAppDirEntry
* and native DirectoryEntry. UI components should be able to display any
* implementation of FilesAppEntry.
*
* FilesAppDirEntry represents a DirectoryEntry-like (folder or root) in the
* Files app. It's a specialization of FilesAppEntry extending the behavior for
* folder, which is basically the method createReader.
* As in FilesAppEntry, FilesAppDirEntry should be interchangeable with Entry
* and DirectoryEntry.
*
* @interface
*/
class FilesAppDirEntry extends FilesAppEntry {
constructor() {
super();
/**
* @public {!boolean} true if this entry represents a Directory-like entry,
* as in have sub-entries and implements {createReader} method.
* Implementations of FilesAppEntry are expected to have this as |true|.
* This attribute is defined on Entry.
*/
this.isDirectory = true;
this.type_name = 'FilesAppDirEntry';
}
/**
* @return {!StaticReader|!DirectoryReader} Returns a reader compatible with
* DirectoryEntry.createReader (from Web Standards) that reads the children of
* this instance.
* This method is defined on DirectoryEntry.
*/
createReader() {}
}
/**
* EntryList, a DirectoryEntry-like object that contains entries. Initially used
* to implement "My Files" containing VolumeEntry for "Downloads", "Linux
* Files" and "Play Files".
*
* @implements FilesAppDirEntry
*/
class EntryList {
/**
* @param {string} label: Label to be used when displaying to user, it should
* already translated.
* @param {VolumeManagerCommon.RootType} rootType root type.
*
*/
constructor(label, rootType) {
/**
* @private {string} label: Label to be used when displaying to user, it
* should be already translated. */
this.label_ = label;
/** @private {VolumeManagerCommon.RootType} rootType root type. */
this.rootType_ = rootType;
/**
* @private {!Array<!Entry|!FilesAppEntry|!FakeEntry>} children entries of
* this EntryList instance.
*/
this.children_ = [];
this.isDirectory = true;
this.isFile = false;
this.type_name = 'EntryList';
}
get children() {
return this.children_;
}
get label() {
return this.label_;
}
get rootType() {
return this.rootType_;
}
get name() {
return this.label_;
}
/** @override */
get isNativeType() {
return false;
}
/** @override */
getMetadata(success, error) {
// Defaults modificationTime to current time just to have a valid value.
setTimeout(() => success({modificationTime: new Date()}));
}
/**
* @return {!string} used to compare entries.
* @override
*/
toURL() {
return 'entry-list://' + this.rootType;
}
/**
* @param {function(Entry)|function(FilesAppEntry)} success callback, it
* returns itself since EntryList is intended to be used as root node and the
* Web Standard says to do so.
* @param {function(Entry)|function(FilesAppEntry)} error callback, not used
* for this implementation.
*
* @override
*/
getParent(success, error) {
setTimeout(success, 0, this);
}
/**
* @param {!Entry|!FakeEntry|!FilesAppEntry} entry that should be added as
* child of this EntryList.
* This method is specific to EntryList instance.
*/
addEntry(entry) {
this.children_.push(entry);
}
/**
* @return {!StaticReader} Returns a reader compatible with
* DirectoryEntry.createReader (from Web Standards) that reads the children of
* this EntryList instance.
* This method is defined on DirectoryEntry.
* @override
*/
createReader() {
return new StaticReader(this.children_);
}
}
/**
* A DirectoryEntry-like which represents a Volume, based on VolumeInfo.
*
* It uses composition to behave like a DirectoryEntry and proxies some calls
* to its VolumeInfo instance.
*
* It's used to be able to add a volume as child of |EntryList| and make volume
* displayable on file list.
*
* @implements FilesAppDirEntry
*/
class VolumeEntry {
/**
* @param {!VolumeInfo} volumeInfo: VolumeInfo for this entry.
*/
constructor(volumeInfo) {
/**
* @private {!VolumeInfo} holds a reference to VolumeInfo to delegate some
* method calls to it.
*/
this.volumeInfo_ = volumeInfo;
/** @type {DirectoryEntry} from Volume's root. */
this.rootEntry_ = volumeInfo.displayRoot;
this.type_name = 'VolumeEntry';
}
/**
* @return {!VolumeInfo} for this entry. This method is only valid for
* VolumeEntry instances.
*/
get volumeInfo() {
return this.volumeInfo_;
}
/**
* @return {DirectoryEntry} for this volume. This method is only valid for
* VolumeEntry instances.
*/
get rootEntry() {
return this.rootEntry_;
}
/**
* @return {!FileSystem} FileSystem for this volume.
* This method is defined on Entry.
*/
get filesystem() {
return this.rootEntry_.filesystem;
}
/**
* @return {!string} Full path for this volume.
* This method is defined on Entry.
* @override.
*/
get fullPath() {
return this.rootEntry_.fullPath;
}
get isDirectory() {
return this.rootEntry_.isDirectory;
}
get isFile() {
return this.rootEntry_.isFile;
}
/**
* @return {!string} Name for this volume.
* @override.
*/
get name() {
return this.volumeInfo_.label;
}
/**
* @return {string}
* @override
*/
toURL() {
return this.rootEntry_.toURL();
}
/**
* @param {function(Entry)|function(FilesAppEntry)} success callback, it
* returns itself since EntryList is intended to be used as root node and the
* Web Standard says to do so.
* @param {function(Entry)|function(FilesAppEntry)} error callback, not used
* for this implementation.
*
* @override
*/
getParent(success, error) {
setTimeout(success, 0, this);
}
/** @override */
getMetadata(success, error) {
this.rootEntry_.getMetadata(success, error);
}
/** @override */
get isNativeType() {
return true;
}
/**
* @return {!StaticReader|!DirectoryReader} Returns a reader from root entry,
* which is compatible with DirectoryEntry.createReader (from Web Standards).
* This method is defined on DirectoryEntry.
* @override
*/
createReader() {
return this.rootEntry_.createReader();
}
}
<!DOCTYPE html>
<!-- Copyright 2018 The Chromium Authors. All rights reserved.
-- Use of this source code is governed by a BSD-style license that can be
-- found in the LICENSE file.
-->
<html>
<body>
<script src="../../common/js/unittest_util.js"></script>
<script src="files_app_entry_types.js"></script>
<script src="files_app_entry_types_unittest.js"></script>
</body>
</html>
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/** Test constructor and default public attributes. */
function testEntryList(testReportCallback) {
const entryList = new EntryList('My Files', 'my_files');
assertEquals('My Files', entryList.label);
assertEquals('entry-list://my_files', entryList.toURL());
assertEquals('my_files', entryList.rootType);
assertFalse(entryList.isNativeType);
assertEquals(0, entryList.children.length);
assertTrue(entryList.isDirectory);
assertFalse(entryList.isFile);
entryList.addEntry(new EntryList('Child Entry', 'child_entry'));
assertEquals(1, entryList.children.length);
const reader = entryList.createReader();
// How many times the reader callback |accumulateResults| has been called?
let callCounter = 0;
// How many times it was called with results?
let resultCouter = 0;
const accumulateResults = (readerResult) => {
// It's called with readerResult==[] a last time to indicate no more files.
callCounter++;
if (readerResult.length > 0) {
resultCouter++;
reader.readEntries(accumulateResults);
}
};
reader.readEntries(accumulateResults);
// readEntries runs asynchronously, so let's wait it to be called.
reportPromise(
waitUntil(() => {
// accumulateResults should be called 2x in normal conditions;
return callCounter >= 2;
}).then(() => {
// Now we can check the final result.
assertEquals(2, callCounter);
assertEquals(1, resultCouter);
}),
testReportCallback);
}
/** Tests method EntryList.getParent. */
function testEntryListGetParent(testReportCallback) {
const entryList = new EntryList('My Files', 'my_files');
let callbackTriggered = false;
entryList.getParent(parentEntry => {
// EntryList should return itself since it's a root and that's what the web
// spec says.
callbackTriggered = true;
assertEquals(parentEntry, entryList);
});
reportPromise(waitUntil(() => callbackTriggered), testReportCallback);
}
/** Tests method EntryList.addEntry. */
function testEntryListAddEntry() {
const entryList = new EntryList('My Files');
assertEquals(0, entryList.children.length);
const fakeRootEntry = createFakeDisplayRoot();
const fakeVolumeInfo = {
displayRoot: fakeRootEntry,
label: 'Fake Filesystem',
};
const childEntry = new VolumeEntry(fakeVolumeInfo);
entryList.addEntry(childEntry);
assertEquals(1, entryList.children.length);
assertEquals(childEntry, entryList.children[0]);
}
/** Tests method EntryList.getMetadata. */
function testEntryListAddVolume(testReportCallback) {
const entryList = new EntryList('My Files');
let modificationTime = null;
entryList.getMetadata(metadata => {
modificationTime = metadata.modificationTime;
});
// getMetadata runs asynchronously, so let's wait it to be called.
reportPromise(
waitUntil(() => {
return modificationTime !== null;
}).then(() => {
// Now we can check the final result, it returns "now", so let's just
// check the type and 1 attribute here.
assertTrue(modificationTime instanceof Date);
assertTrue(!!modificationTime.getUTCFullYear());
}),
testReportCallback);
}
/** Tests StaticReader.readEntries. */
function testStaticReader(testReportCallback) {
const reader = new StaticReader(['file1', 'file2']);
const testResults = [];
// How many times the reader callback |accumulateResults| has been called?
let callCounter = 0;
const accumulateResults = (readerResult) => {
callCounter++;
// merge on testResults.
readerResult.map(f => testResults.push(f));
if (readerResult.length > 0)
reader.readEntries(accumulateResults);
};
reader.readEntries(accumulateResults);
// readEntries runs asynchronously, so let's wait it to be called.
reportPromise(
waitUntil(() => {
// accumulateResults should be called 2x in normal conditions;
return callCounter >= 2;
}).then(() => {
// Now we can check the final result.
assertEquals(2, callCounter);
assertEquals(2, testResults.length);
assertEquals('file1', testResults[0]);
assertEquals('file2', testResults[1]);
}),
testReportCallback);
}
/**
* Returns an object that can be used as displayRoot on a FakeVolumeInfo.
* VolumeEntry delegates many attributes and methods to displayRoot.
*/
function createFakeDisplayRoot() {
const fakeRootEntry = {
filesystem: 'fake-filesystem://',
fullPath: '/fake/full/path',
isDirectory: true,
isFile: false,
name: 'fs-name',
toURL: () => {
return 'fake-filesystem://fake/full/path';
},
createReader: () => {
return 'FAKE READER';
},
getMetadata: (success, error) => {
// Returns static date as modificationTime for testing.
setTimeout(
() => success({modificationTime: new Date(Date.UTC(2018, 6, 27))}));
},
};
return fakeRootEntry;
}
/**
* Tests VolumeEntry constructor and default public attributes/getter/methods.
*/
function testVolumeEntry() {
const fakeRootEntry = createFakeDisplayRoot();
const fakeVolumeInfo = {
displayRoot: fakeRootEntry,
label: 'Fake Filesystem',
};
const volumeEntry = new VolumeEntry(fakeVolumeInfo);
assertEquals(fakeRootEntry, volumeEntry.rootEntry);
assertEquals('fake-filesystem://', volumeEntry.filesystem);
assertEquals('/fake/full/path', volumeEntry.fullPath);
assertEquals('fake-filesystem://fake/full/path', volumeEntry.toURL());
assertEquals('Fake Filesystem', volumeEntry.name);
assertEquals('FAKE READER', volumeEntry.createReader());
assertTrue(volumeEntry.isNativeType);
assertTrue(volumeEntry.isDirectory);
assertFalse(volumeEntry.isFile);
}
/** Tests VolumeEntry.getParent */
function testVolumeEntryGetParent(testReportCallback) {
const fakeRootEntry = createFakeDisplayRoot();
const fakeVolumeInfo = {
displayRoot: fakeRootEntry,
label: 'Fake Filesystem',
};
const volumeEntry = new VolumeEntry(fakeVolumeInfo);
let callbackTriggered = false;
volumeEntry.getParent(parentEntry => {
callbackTriggered = true;
// VolumeEntry should return itself since it's a root and that's what the
// web spec says.
assertEquals(parentEntry, volumeEntry);
});
reportPromise(waitUntil(() => callbackTriggered), testReportCallback);
}
/** Tests VolumeEntry.getMetadata */
function testVolumeEntryGetMetadata(testReportCallback) {
const fakeRootEntry = createFakeDisplayRoot();
const fakeVolumeInfo = {
displayRoot: fakeRootEntry,
label: 'Fake Filesystem',
};
const volumeEntry = new VolumeEntry(fakeVolumeInfo);
let modificationTime = null;
volumeEntry.getMetadata(metadata => {
modificationTime = metadata.modificationTime;
});
// getMetadata runs asynchronously, so let's wait it to be called.
reportPromise(
waitUntil(() => {
return modificationTime !== null;
}).then(() => {
// Now we can check the final result.
assertEquals(2018, modificationTime.getUTCFullYear());
// Date() month is 0-based, so 6 == July. :-(
assertEquals(6, modificationTime.getUTCMonth());
assertEquals(27, modificationTime.getUTCDate());
}),
testReportCallback);
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment