From f86883de6cf090a851e0f81296cb4e36db60f1c5 Mon Sep 17 00:00:00 2001 From: Jordi Bunster Date: Sun, 14 Mar 2021 20:10:48 -0700 Subject: [PATCH] MostRecentActivity changes (#463) This changes the public API a bit, so that recording activity (the most common use) is less involved. --- .../MostRecentActivity.test.ts | 86 +++++++++++++++++++ .../MostRecentActivity/MostRecentActivity.ts | 38 +++++--- src/Explorer/SplashScreen/SplashScreen.tsx | 86 ++++++++----------- src/Explorer/Tree/ResourceTreeAdapter.tsx | 28 +----- .../ResourceTreeAdapterForResourceToken.tsx | 12 +-- 5 files changed, 153 insertions(+), 97 deletions(-) create mode 100644 src/Explorer/MostRecentActivity/MostRecentActivity.test.ts diff --git a/src/Explorer/MostRecentActivity/MostRecentActivity.test.ts b/src/Explorer/MostRecentActivity/MostRecentActivity.test.ts new file mode 100644 index 000000000..a8e84f3a2 --- /dev/null +++ b/src/Explorer/MostRecentActivity/MostRecentActivity.test.ts @@ -0,0 +1,86 @@ +import { observable } from "knockout"; +import { mostRecentActivity } from "./MostRecentActivity"; + +describe("MostRecentActivity", () => { + const accountId = "some account"; + + beforeEach(() => mostRecentActivity.clear(accountId)); + + it("Has no items at first", () => { + expect(mostRecentActivity.getItems(accountId)).toStrictEqual([]); + }); + + it("Can record collections being opened", () => { + const collectionId = "some collection"; + const databaseId = "some database"; + const collection = { + id: observable(collectionId), + databaseId, + }; + + mostRecentActivity.collectionWasOpened(accountId, collection); + + const activity = mostRecentActivity.getItems(accountId); + expect(activity).toEqual([ + expect.objectContaining({ + collectionId, + databaseId, + }), + ]); + }); + + it("Can record notebooks being opened", () => { + const name = "some notebook"; + const path = "some path"; + const notebook = { name, path }; + + mostRecentActivity.notebookWasItemOpened(accountId, notebook); + + const activity = mostRecentActivity.getItems(accountId); + expect(activity).toEqual([expect.objectContaining(notebook)]); + }); + + it("Filters out duplicates", () => { + const name = "some notebook"; + const path = "some path"; + const notebook = { name, path }; + const sameNotebook = { name, path }; + + mostRecentActivity.notebookWasItemOpened(accountId, notebook); + mostRecentActivity.notebookWasItemOpened(accountId, sameNotebook); + + const activity = mostRecentActivity.getItems(accountId); + expect(activity.length).toEqual(1); + expect(activity).toEqual([expect.objectContaining(notebook)]); + }); + + it("Allows for multiple accounts", () => { + const name = "some notebook"; + const path = "some path"; + const notebook = { name, path }; + + const anotherNotebook = { name: "Another " + name, path }; + const anotherAccountId = "Another " + accountId; + + mostRecentActivity.notebookWasItemOpened(accountId, notebook); + mostRecentActivity.notebookWasItemOpened(anotherAccountId, anotherNotebook); + + expect(mostRecentActivity.getItems(accountId)).toEqual([expect.objectContaining(notebook)]); + expect(mostRecentActivity.getItems(anotherAccountId)).toEqual([expect.objectContaining(anotherNotebook)]); + }); + + it("Can store multiple distinct elements, in FIFO order", () => { + const name = "some notebook"; + const path = "some path"; + const first = { name, path }; + const second = { name: "Another " + name, path }; + const third = { name, path: "Another " + path }; + + mostRecentActivity.notebookWasItemOpened(accountId, first); + mostRecentActivity.notebookWasItemOpened(accountId, second); + mostRecentActivity.notebookWasItemOpened(accountId, third); + + const activity = mostRecentActivity.getItems(accountId); + expect(activity).toEqual([third, second, first].map(expect.objectContaining)); + }); +}); diff --git a/src/Explorer/MostRecentActivity/MostRecentActivity.ts b/src/Explorer/MostRecentActivity/MostRecentActivity.ts index 11f0c21a5..0e77cf690 100644 --- a/src/Explorer/MostRecentActivity/MostRecentActivity.ts +++ b/src/Explorer/MostRecentActivity/MostRecentActivity.ts @@ -1,4 +1,6 @@ +import { CollectionBase } from "../../Contracts/ViewModels"; import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility"; +import { NotebookContentItem } from "../Notebook/NotebookContentItem"; export enum Type { OpenCollection, @@ -6,21 +8,18 @@ export enum Type { } export interface OpenNotebookItem { + type: Type.OpenNotebook; name: string; path: string; } export interface OpenCollectionItem { + type: Type.OpenCollection; databaseId: string; collectionId: string; } -export interface Item { - type: Type; - title: string; - description: string; - data: OpenNotebookItem | OpenCollectionItem; -} +type Item = OpenNotebookItem | OpenCollectionItem; // Update schemaVersion if you are going to change this interface interface StoredData { @@ -32,7 +31,7 @@ interface StoredData { * Stores most recent activity */ class MostRecentActivity { - private static readonly schemaVersion: string = "1"; + private static readonly schemaVersion: string = "2"; private static itemsMaxNumber: number = 5; private storedData: StoredData; constructor() { @@ -92,7 +91,7 @@ class MostRecentActivity { LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData)); } - public addItem(accountId: string, newItem: Item): void { + private addItem(accountId: string, newItem: Item): void { // When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable. // if (!accountId) { // return; @@ -111,6 +110,23 @@ class MostRecentActivity { return this.storedData.itemsMap[accountId] || []; } + public collectionWasOpened(accountId: string, { id, databaseId }: Pick) { + const collectionId = id(); + this.addItem(accountId, { + type: Type.OpenCollection, + databaseId, + collectionId, + }); + } + + public notebookWasItemOpened(accountId: string, { name, path }: Pick) { + this.addItem(accountId, { + type: Type.OpenNotebook, + name, + path, + }); + } + public clear(accountId: string): void { delete this.storedData.itemsMap[accountId]; this.saveToLocalStorage(); @@ -128,11 +144,7 @@ class MostRecentActivity { let index = -1; for (let i = 0; i < itemsArray.length; i++) { const currentItem = itemsArray[i]; - if ( - currentItem.title === item.title && - currentItem.description === item.description && - JSON.stringify(currentItem.data) === JSON.stringify(item.data) - ) { + if (JSON.stringify(currentItem) === JSON.stringify(item)) { index = i; break; } diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index 5fa305b28..acd4d5190 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -217,42 +217,6 @@ export class SplashScreen extends React.Component { return heroes; } - private getItemIcon(item: MostRecentActivity.Item): string { - switch (item.type) { - case MostRecentActivity.Type.OpenCollection: - return CollectionIcon; - case MostRecentActivity.Type.OpenNotebook: - return NotebookIcon; - default: - return null; - } - } - - private onItemClicked(item: MostRecentActivity.Item) { - switch (item.type) { - case MostRecentActivity.Type.OpenCollection: { - const openCollectionitem = item.data as MostRecentActivity.OpenCollectionItem; - const collection = this.container.findCollection( - openCollectionitem.databaseId, - openCollectionitem.collectionId - ); - if (collection) { - collection.openTab(); - } - break; - } - case MostRecentActivity.Type.OpenNotebook: { - const openNotebookItem = item.data as MostRecentActivity.OpenNotebookItem; - const notebookItem = this.container.createNotebookContentItemFile(openNotebookItem.name, openNotebookItem.path); - notebookItem && this.container.openNotebook(notebookItem); - break; - } - default: - console.error("Unknown item type", item); - break; - } - } - private createCommonTaskItems(): SplashScreenItem[] { const items: SplashScreenItem[] = []; @@ -333,23 +297,45 @@ export class SplashScreen extends React.Component { return items; } - private static getInfo(item: MostRecentActivity.Item): string { - if (item.type === MostRecentActivity.Type.OpenNotebook) { - const data = item.data as MostRecentActivity.OpenNotebookItem; - return data.path; - } else { - return undefined; - } + private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) { + return { + iconSrc: NotebookIcon, + title: collectionId, + description: "Data", + onClick: () => { + const collection = this.container.findCollection(databaseId, collectionId); + collection && collection.openTab(); + }, + }; + } + + private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) { + return { + info: path, + iconSrc: CollectionIcon, + title: name, + description: "Notebook", + onClick: () => { + const notebookItem = this.container.createNotebookContentItemFile(name, path); + notebookItem && this.container.openNotebook(notebookItem); + }, + }; } private createRecentItems(): SplashScreenItem[] { - return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((item) => ({ - iconSrc: this.getItemIcon(item), - title: item.title, - description: item.description, - info: SplashScreen.getInfo(item), - onClick: () => this.onItemClicked(item), - })); + return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => { + switch (activity.type) { + default: { + const unknownActivity: never = activity; + throw new Error(`Unknown activity: ${unknownActivity}`); + } + case MostRecentActivity.Type.OpenNotebook: + return this.decorateOpenNotebookActivity(activity); + + case MostRecentActivity.Type.OpenCollection: + return this.decorateOpenCollectionActivity(activity); + } + }); } private createTipsItems(): SplashScreenItem[] { diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 46831c9de..06a7d51d4 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -6,7 +6,7 @@ import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } from ".. import * as ViewModels from "../../Contracts/ViewModels"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory"; -import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity"; +import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; @@ -264,15 +264,7 @@ export class ResourceTreeAdapter implements ReactAdapter { onClick: () => { collection.openTab(); // push to most recent - MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, { - type: MostRecentActivity.Type.OpenCollection, - title: collection.id(), - description: "Data", - data: { - databaseId: collection.databaseId, - collectionId: collection.id(), - }, - }); + mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); }, isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id(), [ @@ -573,7 +565,7 @@ export class ResourceTreeAdapter implements ReactAdapter { (item: NotebookContentItem) => { this.container.openNotebook(item).then((hasOpened) => { if (hasOpened) { - this.pushItemToMostRecent(item); + mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); } }); }, @@ -594,7 +586,7 @@ export class ResourceTreeAdapter implements ReactAdapter { (item: NotebookContentItem) => { this.container.openNotebook(item).then((hasOpened) => { if (hasOpened) { - this.pushItemToMostRecent(item); + mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); } }); }, @@ -624,18 +616,6 @@ export class ResourceTreeAdapter implements ReactAdapter { return gitHubNotebooksTree; } - private pushItemToMostRecent(item: NotebookContentItem) { - MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, { - type: MostRecentActivity.Type.OpenNotebook, - title: item.name, - description: "Notebook", - data: { - name: item.name, - path: item.path, - }, - }); - } - private buildChildNodes( item: NotebookContentItem, onFileClick: (item: NotebookContentItem) => void, diff --git a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx b/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx index ca971a1a1..2c106e6f9 100644 --- a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx @@ -1,5 +1,5 @@ import * as ko from "knockout"; -import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity"; +import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import * as React from "react"; import * as ViewModels from "../../Contracts/ViewModels"; import { NotebookContentItem } from "../Notebook/NotebookContentItem"; @@ -44,15 +44,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter { onClick: () => { collection.onDocumentDBDocumentsClick(); // push to most recent - MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, { - type: MostRecentActivity.Type.OpenCollection, - title: collection.id(), - description: "Data", - data: { - databaseId: collection.databaseId, - collectionId: collection.id(), - }, - }); + mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); }, isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id(), ViewModels.CollectionTabKind.Documents),