Skip to content

Commit

Permalink
feat: support recycle bin in content navigation #152 (#190)
Browse files Browse the repository at this point in the history
* feat: support recycle bin in content navigation #152
* add modal confirmation notification before delete item permanently or empty recycle bin
* sync the file content when it was recycled
* delete resource instead of member
* always fetch etag before rename
  • Loading branch information
boyce-w authored Apr 6, 2023
1 parent 4006fac commit 6befa75
Show file tree
Hide file tree
Showing 12 changed files with 375 additions and 121 deletions.
10 changes: 5 additions & 5 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"version": "0.0.1",
"publisher": "SAS",
"engines": {
"vscode": "^1.66.0"
"vscode": "^1.67.0"
},
"dependencies": {
"axios": "^0.27.2",
Expand All @@ -17,7 +17,7 @@
},
"devDependencies": {
"@types/ssh2": "^1.11.7",
"@types/vscode": "^1.66.0",
"@types/vscode": "1.67.0",
"@vscode/test-electron": "^1.6.2"
}
}
92 changes: 89 additions & 3 deletions client/src/components/ContentNavigator/ContentDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ import {
FileSystemProvider,
FileType,
ProviderResult,
TextDocumentContentProvider,
ThemeIcon,
TreeDataProvider,
TreeItem,
TreeItemCollapsibleState,
Uri,
window,
Tab,
TabInputText,
} from "vscode";
import { ContentModel } from "./ContentModel";
import { ContentItem } from "./types";
Expand All @@ -26,18 +30,24 @@ import {
getUri,
isContainer as getIsContainer,
resourceType,
getLink,
} from "./utils";

class ContentDataProvider
implements TreeDataProvider<ContentItem>, FileSystemProvider
implements
TreeDataProvider<ContentItem>,
FileSystemProvider,
TextDocumentContentProvider
{
private _onDidChangeFile: EventEmitter<FileChangeEvent[]>;
private _onDidChangeTreeData: EventEmitter<ContentItem | undefined>;
private _onDidChange: EventEmitter<Uri>;
private readonly model: ContentModel;

constructor(model: ContentModel) {
this._onDidChangeFile = new EventEmitter<FileChangeEvent[]>();
this._onDidChangeTreeData = new EventEmitter<ContentItem | undefined>();
this._onDidChange = new EventEmitter<Uri>();
this.model = model;
}

Expand All @@ -49,6 +59,10 @@ class ContentDataProvider
return this._onDidChangeTreeData.event;
}

get onDidChange(): Event<Uri> {
return this._onDidChange.event;
}

public async connect(baseUrl: string): Promise<void> {
await this.model.connect(baseUrl);
this.refresh();
Expand All @@ -75,6 +89,11 @@ class ContentDataProvider
};
}

public async provideTextDocumentContent(uri: Uri): Promise<string> {
// use text document content provider to display the readonly editor for the files in the recycle bin
return await this.model.getContentByUri(uri);
}

public getChildren(item?: ContentItem): ProviderResult<ContentItem[]> {
return this.model.getChildren(item);
}
Expand Down Expand Up @@ -102,8 +121,8 @@ class ContentDataProvider
.then((content) => new TextEncoder().encode(content));
}

public getUri(item: ContentItem): Promise<Uri> {
return this.model.getUri(item);
public getUri(item: ContentItem, readOnly: boolean): Promise<Uri> {
return this.model.getUri(item, readOnly);
}

public async createFolder(
Expand Down Expand Up @@ -132,6 +151,9 @@ class ContentDataProvider
item: ContentItem,
name: string
): Promise<Uri | undefined> {
if (!(await closeFileIfOpen(getUri(item, item.__trash__)))) {
return;
}
const newItem = await this.model.renameResource(item, name);
if (newItem) {
return getUri(newItem);
Expand All @@ -143,11 +165,63 @@ class ContentDataProvider
}

public async deleteResource(item: ContentItem): Promise<boolean> {
if (!(await closeFileIfOpen(getUri(item, item.__trash__)))) {
return false;
}
const success = await this.model.delete(item);
if (success) {
this.refresh();
}
return success;
}

public async recycleResource(item: ContentItem): Promise<boolean> {
const recycleBin = this.model.getDelegateFolder("@myRecycleBin");
if (!recycleBin) {
// fallback to delete
return this.deleteResource(item);
}
const recycleBinUri = getLink(recycleBin.links, "GET", "self")?.uri;
if (!recycleBinUri) {
return false;
}
if (!(await closeFileIfOpen(getUri(item, item.__trash__)))) {
return false;
}
const success = await this.model.moveTo(item, recycleBinUri);
if (success) {
this.refresh();
// update the text document content as well just in case that this file was just restored and updated
this._onDidChange.fire(getUri(item, true));
}
return success;
}

public async restoreResource(item: ContentItem): Promise<boolean> {
const previousParentUri = getLink(item.links, "GET", "previousParent")?.uri;
if (!previousParentUri) {
return false;
}
if (!(await closeFileIfOpen(getUri(item, item.__trash__)))) {
return false;
}
const success = await this.model.moveTo(item, previousParentUri);
if (success) {
this.refresh();
}
return success;
}

public async emptyRecycleBin(): Promise<boolean> {
const recycleBin = this.model.getDelegateFolder("@myRecycleBin");
const children = await this.getChildren(recycleBin);
const result = await Promise.all(
children.map((child) => this.deleteResource(child))
);
const success = result.length === children.length;
if (success) {
this.refresh();
}
return success;
}

Expand Down Expand Up @@ -181,3 +255,15 @@ class ContentDataProvider
}

export default ContentDataProvider;

const closeFileIfOpen = (file: Uri): boolean | Thenable<boolean> => {
const tabs: Tab[] = window.tabGroups.all.map((tg) => tg.tabs).flat();
const tab = tabs.find(
(tab) =>
tab.input instanceof TabInputText && tab.input.uri.query === file.query // compare the file id
);
if (tab) {
return window.tabGroups.close(tab);
}
return true;
};
Loading

0 comments on commit 6befa75

Please sign in to comment.