Skip to content

Commit be613c3

Browse files
committed
Added checkboxes
1 parent 70dab38 commit be613c3

File tree

10 files changed

+341
-25
lines changed

10 files changed

+341
-25
lines changed

package-lock.json

Lines changed: 55 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,6 +1536,7 @@
15361536
"@segment/analytics-node": "^2.1.3",
15371537
"@testing-library/react": "^12.1.5",
15381538
"@types/mustache": "^4.0.1",
1539+
"@vscode-elements/elements": "^1.14.0",
15391540
"@vscode/webview-ui-toolkit": "^1.4.0",
15401541
"awesome-debounce-promise": "^2.1.0",
15411542
"axios": "^1.7.4",

src/container.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import { Logger } from './logger';
7575
import { SearchJiraHelper } from './views/jira/searchJiraHelper';
7676
import { featureFlagClientInitializedEvent } from './analytics';
7777
import { openPullRequest } from './commands/bitbucket/pullRequest';
78+
import { CheckboxStateManager } from './views/nodes/checkBoxStateManager';
7879

7980
const isDebuggingRegex = /^--(debug|inspect)\b(-brk\b|(?!-))=?/;
8081
const ConfigTargetKey = 'configurationTarget';
@@ -87,6 +88,8 @@ export class Container {
8788
static async initialize(context: ExtensionContext, config: IConfig, version: string) {
8889
const analyticsEnv: string = this.isDebugging ? 'staging' : 'prod';
8990

91+
this._checkboxStateManager = new CheckboxStateManager(context);
92+
9093
this._analyticsClient = analyticsClient({
9194
origin: 'desktop',
9295
env: analyticsEnv,
@@ -531,4 +534,9 @@ export class Container {
531534
public static get pmfStats() {
532535
return this._pmfStats;
533536
}
537+
538+
private static _checkboxStateManager: CheckboxStateManager;
539+
public static get checkboxStateManager() {
540+
return this._checkboxStateManager;
541+
}
534542
}

src/views/BitbucketExplorer.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,46 @@ import { DescriptionNode, PullRequestTitlesNode } from './pullrequest/pullReques
1212
import { PullRequestNodeDataProvider } from './pullRequestNodeDataProvider';
1313
import { RefreshTimer } from './RefreshTimer';
1414
import { BitbucketActivityMonitor } from './BitbucketActivityMonitor';
15+
import { PullRequestFilesNode } from './nodes/pullRequestFilesNode';
16+
import { DirectoryNode } from './nodes/directoryNode';
17+
import { TreeView } from 'vscode';
18+
import { AbstractBaseNode } from './nodes/abstractBaseNode';
1519

1620
export abstract class BitbucketExplorer extends Explorer implements Disposable {
1721
private _disposable: Disposable;
1822

1923
private monitor: BitbucketActivityMonitor | undefined;
2024
private _refreshTimer: RefreshTimer;
25+
private _onDidChangeTreeData = new vscode.EventEmitter<AbstractBaseNode | undefined>();
26+
27+
protected newTreeView(): TreeView<AbstractBaseNode> | undefined {
28+
super.newTreeView();
29+
this.setupCheckboxHandling();
30+
return this.treeView;
31+
}
32+
33+
private setupCheckboxHandling(): void {
34+
if (!this.treeView) {
35+
return;
36+
}
37+
this.treeView.onDidChangeCheckboxState((event) => {
38+
event.items.forEach(([item, state]) => {
39+
const checked = state === vscode.TreeItemCheckboxState.Checked;
40+
if (item instanceof PullRequestFilesNode || item instanceof DirectoryNode) {
41+
item.checked = checked;
42+
this._onDidChangeTreeData.fire(item);
43+
}
44+
});
45+
});
46+
}
2147

2248
constructor(protected ctx: BitbucketContext) {
2349
super(() => this.dispose());
2450

51+
setTimeout(() => {
52+
this.setupCheckboxHandling();
53+
}, 50);
54+
2555
Container.context.subscriptions.push(configuration.onDidChange(this._onConfigurationChanged, this));
2656

2757
this._refreshTimer = new RefreshTimer(this.explorerEnabledConfiguration(), this.refreshConfiguration(), () =>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Logger } from 'src/logger';
2+
import vscode from 'vscode';
3+
interface CheckboxState {
4+
id: string;
5+
timestamp: number;
6+
}
7+
export class CheckboxStateManager {
8+
private readonly STATE_EXPIRY = 24 * 60 * 60 * 1000;
9+
private readonly STORAGE_KEY = 'bitbucked.viewedFiles';
10+
11+
constructor(private context: vscode.ExtensionContext) {
12+
this.cleanup();
13+
}
14+
15+
private async cleanup(): Promise<void> {
16+
const states = this.context.workspaceState.get<CheckboxState[]>(this.STORAGE_KEY, []);
17+
const now = Date.now();
18+
19+
const validStates = states.filter((state) => {
20+
const isValid = now - state.timestamp < this.STATE_EXPIRY;
21+
if (!isValid) {
22+
Logger.debug(`Removing expired checkbox state: ${state.id}`);
23+
}
24+
return isValid;
25+
});
26+
27+
await this.context.workspaceState.update(this.STORAGE_KEY, validStates);
28+
Logger.debug(`Cleanup complete. Remaining states: ${validStates.length}`);
29+
}
30+
31+
isChecked(id: string): boolean {
32+
const states = this.context.workspaceState.get<CheckboxState[]>(this.STORAGE_KEY, []);
33+
const state = states.find((state) => state.id === id);
34+
35+
if (state) {
36+
if (Date.now() - state.timestamp >= this.STATE_EXPIRY) {
37+
this.setChecked(id, false);
38+
return false;
39+
}
40+
return true;
41+
}
42+
return false;
43+
}
44+
45+
setChecked(id: string, checked: boolean): void {
46+
const states = this.context.workspaceState.get<CheckboxState[]>(this.STORAGE_KEY, []);
47+
48+
if (checked) {
49+
const existingIndex = states.findIndex((state) => state.id === id);
50+
if (existingIndex !== -1) {
51+
states.splice(existingIndex, 1);
52+
}
53+
states.push({ id, timestamp: Date.now() });
54+
} else {
55+
const index = states.findIndex((state) => state.id === id);
56+
if (index !== -1) {
57+
states.splice(index, 1);
58+
}
59+
}
60+
61+
this.context.workspaceState.update(this.STORAGE_KEY, states);
62+
Logger.debug(`Checkbox state updated: ${id} = ${checked}`);
63+
}
64+
}

src/views/nodes/commitNode.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,18 @@ export class CommitNode extends AbstractBaseNode {
3636
//TODO: pass tasks if commit-level tasks exist
3737
//TODO: if there is more than one parent, there should probably be a notification about diff ambiguity, unless I can figure
3838
//out a way to resolve this
39-
const children = await createFileChangesNodes(this.pr, paginatedComments, diffs, conflictedFiles, [], {
40-
lhs: this.commit.parentHashes?.[0] ?? '', //The only time I can think of this being undefined is for an initial commit, but what should the parent be there?
41-
rhs: this.commit.hash,
42-
});
39+
const children = await createFileChangesNodes(
40+
this.pr,
41+
paginatedComments,
42+
diffs,
43+
conflictedFiles,
44+
[],
45+
{
46+
lhs: this.commit.parentHashes?.[0] ?? '', //The only time I can think of this being undefined is for an initial commit, but what should the parent be there?
47+
rhs: this.commit.hash,
48+
},
49+
'commits',
50+
);
4351
return children;
4452
} catch (e) {
4553
Logger.debug('error fetching changed files', e);

src/views/nodes/directoryNode.ts

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,109 @@ import * as vscode from 'vscode';
22
import { PRDirectory } from '../pullrequest/diffViewHelper';
33
import { AbstractBaseNode } from './abstractBaseNode';
44
import { PullRequestFilesNode } from './pullRequestFilesNode';
5+
import { Container } from 'src/container';
6+
import { Logger } from 'src/logger';
57

68
export class DirectoryNode extends AbstractBaseNode {
7-
constructor(private directoryData: PRDirectory) {
9+
constructor(
10+
private directoryData: PRDirectory,
11+
private prUrl: string,
12+
private section: 'files' | 'commits' = 'files',
13+
private commitHash?: string,
14+
) {
815
super();
916
}
1017

18+
private _isDirectClick = false;
19+
20+
get directoryId(): string {
21+
const prUrlPath = vscode.Uri.parse(this.prUrl).path;
22+
const prId = prUrlPath.slice(prUrlPath.lastIndexOf('/') + 1);
23+
const repoUrl = this.prUrl.slice(0, this.prUrl.indexOf('/pull-requests'));
24+
const repoId = repoUrl.slice(repoUrl.lastIndexOf('/') + 1);
25+
const dirPath = this.directoryData.fullPath;
26+
27+
if (this.directoryData.treeHash) {
28+
return `repo-${repoId}-pr-${prId}-${this.section}-${this.commitHash || 'main'}-tree-${this.directoryData.treeHash}`;
29+
}
30+
return `repo-${repoId}-pr-${prId}-${this.section}-${this.commitHash || 'main'}-directory-${dirPath}`;
31+
}
32+
33+
private areAllChildrenChecked(): boolean {
34+
const allFilesChecked = this.directoryData.files.every((file) => {
35+
const fileNode = new PullRequestFilesNode(file, this.section, this.commitHash);
36+
return fileNode.checked;
37+
});
38+
39+
const allSubdirsChecked = Array.from(this.directoryData.subdirs.values()).every((subdir) => {
40+
const subdirNode = new DirectoryNode(subdir, this.prUrl, this.section, this.commitHash);
41+
return subdirNode.checked;
42+
});
43+
44+
return allFilesChecked && allSubdirsChecked;
45+
}
46+
47+
set checked(value: boolean) {
48+
Logger.debug(`Setting directory ${this.directoryId} checked state to ${value}`);
49+
Container.checkboxStateManager.setChecked(this.directoryId, value);
50+
if (this._isDirectClick) {
51+
Logger.debug('Propagating state to children');
52+
this.directoryData.files.forEach((file) => {
53+
const fileNode = new PullRequestFilesNode(file, this.section, this.commitHash);
54+
Container.checkboxStateManager.setChecked(fileNode.fileId, value);
55+
});
56+
this.directoryData.subdirs.forEach((subdir) => {
57+
const subdirNode = new DirectoryNode(subdir, this.prUrl, this.section, this.commitHash);
58+
Container.checkboxStateManager.setChecked(subdirNode.directoryId, value);
59+
});
60+
}
61+
}
62+
get checked(): boolean {
63+
return Container.checkboxStateManager.isChecked(this.directoryId);
64+
}
65+
1166
async getTreeItem(): Promise<vscode.TreeItem> {
12-
const item = new vscode.TreeItem(this.directoryData.name, vscode.TreeItemCollapsibleState.Expanded);
67+
// For root files folder we do not want to show a checkbox and we want to show the folder icon and we do not want it expanded
68+
const isRootFilesDirectory =
69+
this.section === 'files' && this.directoryData.name === 'Files' && this.directoryData.fullPath === '';
70+
71+
const item = new vscode.TreeItem(
72+
this.directoryData.name,
73+
isRootFilesDirectory ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.Expanded,
74+
);
1375
item.tooltip = this.directoryData.name;
14-
item.iconPath = vscode.ThemeIcon.Folder;
76+
77+
if (!isRootFilesDirectory) {
78+
item.iconPath = vscode.ThemeIcon.Folder;
79+
}
80+
81+
const allChecked = this.areAllChildrenChecked();
82+
if (!this._isDirectClick) {
83+
Container.checkboxStateManager.setChecked(this.directoryId, allChecked);
84+
}
85+
86+
if (!isRootFilesDirectory) {
87+
item.checkboxState = this.checked
88+
? vscode.TreeItemCheckboxState.Checked
89+
: vscode.TreeItemCheckboxState.Unchecked;
90+
item.contextValue = `directory${allChecked ? '.checked' : ''}`;
91+
}
92+
93+
item.id = this.directoryId;
94+
1595
return item;
1696
}
1797

18-
async getChildren(element?: AbstractBaseNode): Promise<AbstractBaseNode[]> {
98+
async getChildren(): Promise<AbstractBaseNode[]> {
99+
const fileNodes: AbstractBaseNode[] = this.directoryData.files.map(
100+
(diffViewArg) => new PullRequestFilesNode(diffViewArg, this.section, this.commitHash),
101+
);
102+
19103
const directoryNodes: DirectoryNode[] = Array.from(
20104
this.directoryData.subdirs.values(),
21-
(subdir) => new DirectoryNode(subdir),
105+
(subdir) => new DirectoryNode(subdir, this.prUrl, this.section, this.commitHash),
22106
);
23-
const fileNodes: AbstractBaseNode[] = this.directoryData.files.map(
24-
(diffViewArg) => new PullRequestFilesNode(diffViewArg),
25-
);
26-
return fileNodes.concat(directoryNodes);
107+
108+
return [...fileNodes, ...directoryNodes];
27109
}
28110
}

0 commit comments

Comments
 (0)