Extension: Merge show team relevant repo management buttons (#17)

* Extension: Show team relevant repo management buttons

* fix formatting

* Fix: Actually delete team repo contents

Was passing incomplete path to remove API

* Extra logging

* Formatting

* Fix path on windows

* Remove logs

---------

Co-authored-by: orosmatthew <orosmatthew@pm.me>
This commit is contained in:
David Poeschl 2024-03-13 10:13:09 -07:00 committed by GitHub
parent 40634d80e6
commit 42a58afbde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 469 additions and 104 deletions

View File

@ -68,8 +68,25 @@
{ {
"command": "bwcontest.toggleFastPolling", "command": "bwcontest.toggleFastPolling",
"title": "BWContest Developer: Toggle Fast Polling" "title": "BWContest Developer: Toggle Fast Polling"
},
{
"command": "bwcontest.refreshState",
"title": "Refresh"
},
{
"command": "bwcontest.showTestSubmitPage",
"title": "BWContest: Show Test/Submit Page"
} }
] ],
"menus": {
"view/title": [
{
"command": "bwcontest.refreshState",
"group": "navigation",
"when": "view == bwcontest-sidebar"
}
]
}
}, },
"scripts": { "scripts": {
"vscode:prepublish": "npm run compile", "vscode:prepublish": "npm run compile",

View File

@ -1,6 +1,15 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { getNonce } from './getNonce'; import { getNonce } from './getNonce';
import { cloneAndOpenRepo } from './extension'; import {
RepoState,
clearCachedRepoState,
cloneOpenRepo,
cloneRepo,
openRepo,
getCachedRepoState,
refreshRepoState,
repoStateChanged
} from './teamRepoManager';
import { BWPanel } from './problemPanel'; import { BWPanel } from './problemPanel';
import urlJoin from 'url-join'; import urlJoin from 'url-join';
import outputPanelLog from './outputPanelLog'; import outputPanelLog from './outputPanelLog';
@ -22,12 +31,15 @@ import { startTeamStatusPolling, stopTeamStatusPolling } from './contestMonitor/
export type WebviewMessageType = export type WebviewMessageType =
| { msg: 'onLogin'; data: TeamData } | { msg: 'onLogin'; data: TeamData }
| { msg: 'onLogout' } | { msg: 'onLogout' }
| { msg: 'teamStatusUpdated'; data: SidebarTeamStatus | null }; | { msg: 'teamStatusUpdated'; data: SidebarTeamStatus | null }
| { msg: 'repoStateUpdated'; data: RepoState };
export type MessageType = export type MessageType =
| { msg: 'onTestAndSubmit' } | { msg: 'onTestAndSubmit' }
| { msg: 'onUIMount' } | { msg: 'onUIMount' }
| { msg: 'onClone'; data: { contestId: number; teamId: number } } | { msg: 'onCloneOpenRepo'; data: { contestId: number; teamId: number } }
| { msg: 'onCloneRepo'; data: { contestId: number; teamId: number } }
| { msg: 'onOpenRepo'; data: { contestId: number; teamId: number } }
| { msg: 'onLogin'; data: { teamName: string; password: string } } | { msg: 'onLogin'; data: { teamName: string; password: string } }
| { msg: 'onLogout' }; | { msg: 'onLogout' };
@ -75,6 +87,22 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
submissionsChangedEventArgs.changedProblemIds submissionsChangedEventArgs.changedProblemIds
); );
}); });
const currentRepoState = getCachedRepoState();
outputPanelLog.info(
'When SidebarProvider constructed, cached repo state is: ' + currentRepoState
);
this.updateRepoStatus(currentRepoState);
repoStateChanged.add((repoChangedEventArgs) => {
outputPanelLog.trace('Repo status updating from event');
if (!repoChangedEventArgs) {
return;
}
this.updateRepoStatus(repoChangedEventArgs.state);
});
} }
private async handleLogin( private async handleLogin(
@ -129,6 +157,11 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
'After login, cached submission list is: ' + JSON.stringify(currentSubmissionsList) 'After login, cached submission list is: ' + JSON.stringify(currentSubmissionsList)
); );
this.updateTeamStatus(currentSubmissionsList); this.updateTeamStatus(currentSubmissionsList);
const currentRepoState = getCachedRepoState();
outputPanelLog.info('After login, cached repo state is: ' + currentRepoState);
this.updateRepoStatus(currentRepoState);
refreshRepoState();
} }
private async handleLogout(webviewPostMessage: (m: WebviewMessageType) => void) { private async handleLogout(webviewPostMessage: (m: WebviewMessageType) => void) {
@ -189,6 +222,8 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
stopTeamStatusPolling(); stopTeamStatusPolling();
clearCachedContestTeamState(); clearCachedContestTeamState();
clearCachedRepoState();
this.context.globalState.update('token', undefined); this.context.globalState.update('token', undefined);
this.context.globalState.update('teamData', undefined); this.context.globalState.update('teamData', undefined);
} }
@ -236,6 +271,24 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
this.webview.postMessage(message); this.webview.postMessage(message);
} }
public updateRepoStatus(state: RepoState) {
if (this.webview == null) {
outputPanelLog.trace('Not updating sidebar repo state because webview is null');
return;
}
const message: WebviewMessageType = {
msg: 'repoStateUpdated',
data: state
};
outputPanelLog.trace(
'Posting repoStateUpdated to webview with message: ' + JSON.stringify(message)
);
this.webview.postMessage(message);
}
public resolveWebviewView(webviewView: vscode.WebviewView) { public resolveWebviewView(webviewView: vscode.WebviewView) {
outputPanelLog.trace('SidebarProvider resolveWebviewView'); outputPanelLog.trace('SidebarProvider resolveWebviewView');
const webview = webviewView.webview; const webview = webviewView.webview;
@ -273,11 +326,23 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
'onUIMount, currentSubmissionsList is ' + JSON.stringify(currentSubmissionsList) 'onUIMount, currentSubmissionsList is ' + JSON.stringify(currentSubmissionsList)
); );
this.updateTeamStatus(currentSubmissionsList); this.updateTeamStatus(currentSubmissionsList);
const currentRepoState = getCachedRepoState();
outputPanelLog.trace('onUIMount, currentRepoState is ' + currentRepoState);
this.updateRepoStatus(currentRepoState);
} }
break; break;
} }
case 'onClone': { case 'onCloneOpenRepo': {
cloneAndOpenRepo(m.data.contestId, m.data.teamId); cloneOpenRepo(m.data.contestId, m.data.teamId);
break;
}
case 'onCloneRepo': {
cloneRepo(m.data.contestId, m.data.teamId);
break;
}
case 'onOpenRepo': {
openRepo(m.data.contestId, m.data.teamId);
break; break;
} }
case 'onLogin': { case 'onLogin': {

View File

@ -1,16 +1,17 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { SidebarProvider } from './SidebarProvider'; import { SidebarProvider } from './SidebarProvider';
import * as fs from 'fs-extra';
import urlJoin from 'url-join';
import git from 'isomorphic-git';
import path = require('path');
import http from 'isomorphic-git/http/node';
import outputPanelLog from './outputPanelLog'; import outputPanelLog from './outputPanelLog';
import { import {
startTeamStatusPollingOnActivation, startTeamStatusPollingOnActivation,
stopTeamStatusPolling, stopTeamStatusPolling,
useFastPolling useFastPolling
} from './contestMonitor/pollingService'; } from './contestMonitor/pollingService';
import {
clearCachedRepoState,
refreshRepoState,
setRepoManagerExtensionContext
} from './teamRepoManager';
import { BWPanel } from './problemPanel';
export interface BWContestSettings { export interface BWContestSettings {
repoBaseUrl: string; repoBaseUrl: string;
@ -24,93 +25,6 @@ export function extensionSettings(): BWContestSettings {
return vscode.workspace.getConfiguration().get<BWContestSettings>('BWContest')!; return vscode.workspace.getConfiguration().get<BWContestSettings>('BWContest')!;
} }
function closeAllWorkspaces() {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) {
return;
}
const removedFolders = vscode.workspace.updateWorkspaceFolders(0, workspaceFolders.length);
if (!removedFolders) {
return;
}
}
export async function cloneAndOpenRepo(contestId: number, teamId: number) {
const currentSettings = vscode.workspace.getConfiguration().get<BWContestSettings>('BWContest');
if (!currentSettings || currentSettings.repoBaseUrl == '') {
vscode.window.showErrorMessage('BWContest: BWContest.repoBaseURL not set');
return;
}
if (!currentSettings || currentSettings.repoClonePath == '') {
vscode.window.showErrorMessage('BWContest: BWContest.repoClonePath not set');
return;
}
if (!currentSettings || currentSettings.webUrl == '') {
vscode.window.showErrorMessage('BWContest: BWContest.webUrl not set');
return;
}
const repoUrl = urlJoin(
currentSettings.repoBaseUrl,
contestId.toString(),
`${teamId.toString()}.git`
);
const repoName = teamId.toString();
if (!fs.existsSync(`${currentSettings.repoClonePath}/BWContest`)) {
fs.mkdirSync(`${currentSettings.repoClonePath}/BWContest`);
}
if (!fs.existsSync(`${currentSettings.repoClonePath}/BWContest/${contestId.toString()}`)) {
fs.mkdirSync(`${currentSettings.repoClonePath}/BWContest/${contestId.toString()}`);
}
const clonedRepoPath = `${
currentSettings.repoClonePath
}/BWContest/${contestId.toString()}/${repoName}`;
if (fs.existsSync(clonedRepoPath)) {
const confirm = await vscode.window.showWarningMessage(
'The repo already exists. Do you want to replace it?',
'Delete and Replace',
'Cancel'
);
if (confirm !== 'Delete and Replace') {
return;
}
closeAllWorkspaces();
fs.removeSync(clonedRepoPath);
}
const dir = path.join(currentSettings.repoClonePath, 'BWContest', contestId.toString(), repoName);
outputPanelLog.info(`Running 'git clone' to directory: ${dir}`);
try {
await git.clone({ fs, http, dir, url: repoUrl });
} catch (error) {
outputPanelLog.error(
"Failed to 'git clone'. The git server might be incorrectly configured. Error: " + error
);
throw error;
}
outputPanelLog.info('Closing workspaces...');
closeAllWorkspaces();
const addedFolder = vscode.workspace.updateWorkspaceFolders(
vscode.workspace.workspaceFolders?.length ?? 0,
0,
{ uri: vscode.Uri.file(clonedRepoPath), name: 'BWContest' }
);
if (!addedFolder) {
vscode.window.showErrorMessage('BWContest: Failed to open cloned repo');
return;
}
vscode.window.showInformationMessage('BWContest: Repo cloned and opened');
}
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
outputPanelLog.info('BWContest Extension Activated'); outputPanelLog.info('BWContest Extension Activated');
@ -133,13 +47,27 @@ export function activate(context: vscode.ExtensionContext) {
fastPolling = !fastPolling; fastPolling = !fastPolling;
useFastPolling(fastPolling); useFastPolling(fastPolling);
}),
vscode.commands.registerCommand('bwcontest.showTestSubmitPage', () => {
BWPanel.show(context, extensionSettings().webUrl);
}),
vscode.commands.registerCommand('bwcontest.refreshState', () => {
refreshRepoState();
}) })
); );
startTeamStatusPollingOnActivation(context); startTeamStatusPollingOnActivation(context);
setRepoManagerExtensionContext(context);
refreshRepoState();
vscode.workspace.onDidChangeWorkspaceFolders(() => {
refreshRepoState();
});
} }
export function deactivate() { export function deactivate() {
outputPanelLog.info('BWContest Extension Deactivated'); outputPanelLog.info('BWContest Extension Deactivated');
stopTeamStatusPolling(); stopTeamStatusPolling();
clearCachedRepoState();
} }

View File

@ -0,0 +1,318 @@
import * as vscode from 'vscode';
import * as fs from 'fs-extra';
import urlJoin from 'url-join';
import git from 'isomorphic-git';
import path = require('path');
import http from 'isomorphic-git/http/node';
import outputPanelLog from './outputPanelLog';
import { BWContestSettings } from './extension';
import { LiteEvent } from './utilities/LiteEvent';
import { TeamData } from './sharedTypes';
import * as os from 'os';
let latestRepoState: RepoState = 'No Team';
const onRepoStateChanged = new LiteEvent<RepoChangedEventArgs>();
export const repoStateChanged = onRepoStateChanged.expose();
export type RepoState = 'No Team' | 'No Repo' | 'Repo Exists, Not Open' | 'Repo Open';
export type RepoChangedEventArgs = {
state: RepoState;
};
export function getCachedRepoState(): RepoState {
return latestRepoState;
}
export function clearCachedRepoState(): void {
outputPanelLog.trace(`Clearing cached repoState`);
setRepoState('No Team');
}
export async function refreshRepoState(): Promise<void> {
outputPanelLog.trace(`Refreshing repoState`);
if (!repoManagerExtensionContext) {
outputPanelLog.trace(` -> repoState is 'No Team' because something is misconfigured`);
return setRepoState('No Team');
}
const teamData = repoManagerExtensionContext.globalState.get<TeamData>('teamData');
if (teamData === undefined) {
outputPanelLog.trace(` -> repoState is 'No Team' because no globalState for teamData`);
return setRepoState('No Team');
}
const repoPaths = getRepoPaths(teamData.contestId, teamData.teamId);
if (!repoPaths.success) {
outputPanelLog.trace(` -> repoState is 'No Team' can't calculate repo paths`);
return setRepoState('No Team');
}
const { clonedRepoPath } = repoPaths;
outputPanelLog.trace(` -> inspecting local repoPath ${clonedRepoPath}`);
if (!fs.existsSync(clonedRepoPath)) {
outputPanelLog.trace(` -> repoState is 'No Repo', the local repo path doesn't exist at all`);
return setRepoState('No Repo');
}
if (!(await directoryHasGitRepo(clonedRepoPath))) {
outputPanelLog.trace(
` -> repoState is 'No Repo', the local repo path exists but does not have a git repo`
);
return setRepoState('No Repo');
}
if (vscode.workspace.workspaceFolders) {
const existingOpenFolderForRepo = vscode.workspace.workspaceFolders.filter((f) => {
const p =
os.platform() === 'win32'
? path.normalize(f.uri.path.slice(1))
: path.normalize(f.uri.path);
return p === path.normalize(clonedRepoPath);
})[0];
if (existingOpenFolderForRepo) {
outputPanelLog.trace(
` -> repoState is 'Repo Open', we found the repo path in VSCode's workspaceFolders`
);
return setRepoState('Repo Open');
}
}
const workspaceFoldersLogText = vscode.workspace.workspaceFolders
? vscode.workspace.workspaceFolders.map((f) => f.uri).join(', ')
: '(no workspaceFolders)';
outputPanelLog.trace(
` -> repoState is 'Repo Exists, Not Open', did not find repoPath (${clonedRepoPath}) in VSCode's workspaceFolders (${workspaceFoldersLogText})`
);
return setRepoState('Repo Exists, Not Open');
}
function setRepoState(state: RepoState): void {
if (state != latestRepoState) {
outputPanelLog.trace(`Detected repoState change: ${latestRepoState} -> ${state}`);
latestRepoState = state;
onRepoStateChanged.trigger({ state });
} else {
outputPanelLog.trace(`No repoState change, same value: ${state}`);
}
}
export async function cloneOpenRepo(contestId: number, teamId: number): Promise<boolean> {
const result = (await cloneRepoWorker(contestId, teamId)) && openRepoWorker(contestId, teamId);
refreshRepoState();
return result;
}
export async function cloneRepo(contestId: number, teamId: number): Promise<boolean> {
const result = await cloneRepoWorker(contestId, teamId);
refreshRepoState();
return result;
}
export function openRepo(contestId: number, teamId: number): boolean {
const result = openRepoWorker(contestId, teamId);
refreshRepoState();
return result;
}
async function cloneRepoWorker(contestId: number, teamId: number): Promise<boolean> {
const repoPaths = getRepoPaths(contestId, teamId);
if (!repoPaths.success) {
vscode.window.showErrorMessage('BWContest: BWContestSettings not configured');
return false;
}
const { repoUrl, clonedRepoPath } = repoPaths;
outputPanelLog.trace(`Trying to cloneRepo`);
outputPanelLog.trace(` URL: ${repoUrl}`);
outputPanelLog.trace(` Local Directory: ${clonedRepoPath}`);
if (!fs.existsSync(clonedRepoPath)) {
outputPanelLog.trace('Local Directory does not exist, creating it');
try {
fs.mkdirSync(clonedRepoPath, { recursive: true });
} catch (error) {
vscode.window.showErrorMessage(
`BWContest: Could not create directory '${clonedRepoPath}': ${error}`
);
return false;
}
outputPanelLog.trace('Local Directory created, starting clone');
return await doClone(clonedRepoPath, repoUrl);
}
if (fs.readdirSync(clonedRepoPath).length == 0) {
outputPanelLog.trace('Local Directory exists but is empty, starting clone');
return await doClone(clonedRepoPath, repoUrl);
}
outputPanelLog.trace('Local Directory exists and is non-empty');
const gitHasRemote = await directoryHasGitRepo(clonedRepoPath);
const deleteAndReplacePrompt = gitHasRemote
? 'The repository already exists, replacing it will delete local changes. Are you sure?'
: 'The repository directory exists with no git repo, deleting it will delete local changes. Are you sure?';
const confirm = await vscode.window.showWarningMessage(
deleteAndReplacePrompt,
'Delete and Replace',
'Cancel'
);
if (confirm !== 'Delete and Replace') {
return false;
}
outputPanelLog.trace(`Team has chosen to delete non-empty Local Directory`);
try {
const existingItemsInDir = fs.readdirSync(clonedRepoPath, {
recursive: false,
encoding: 'utf8'
});
outputPanelLog.trace(` Removing ${existingItemsInDir.length} items`);
for (const existingItemInDir of existingItemsInDir) {
const fullPath = path.join(clonedRepoPath, existingItemInDir);
outputPanelLog.trace(` Removing ${fullPath}`);
fs.rmSync(fullPath, { recursive: true, force: true });
}
} catch (error) {
vscode.window.showErrorMessage(
`BWContest: Failed to delete contents of Local Directory '${clonedRepoPath}': ${error}`
);
return false;
}
try {
const itemsInDirAfterDelete = fs.readdirSync(clonedRepoPath, {
recursive: false,
encoding: 'utf8'
});
outputPanelLog.trace(
`Local Directory should now be empty, there are ${itemsInDirAfterDelete.length} item(s): ${itemsInDirAfterDelete.join(', ')}`
);
if (itemsInDirAfterDelete.length > 0) {
vscode.window.showErrorMessage(`BWContest: Failed to delete contents of Local Directory`);
return false;
}
} catch (error) {
vscode.window.showErrorMessage(
`BWContest: Failed to delete contents of Local Directory '${clonedRepoPath}': ${error}`
);
return false;
}
outputPanelLog.trace(`Local Directory is now empty, starting clone`);
return await doClone(clonedRepoPath, repoUrl);
}
async function directoryHasGitRepo(path: string): Promise<boolean> {
try {
await git.listRemotes({ fs, dir: path });
return true;
} catch (error) {
return false;
}
}
async function doClone(targetDirectory: string, repoUrl: string): Promise<boolean> {
outputPanelLog.trace(`Running 'git clone' from url ${repoUrl} to directory: ${targetDirectory}`);
try {
await git.clone({ fs, http, dir: targetDirectory, url: repoUrl });
} catch (error) {
vscode.window.showErrorMessage(`BWContest: Failed to git clone: ${error}`);
return false;
}
vscode.window.showInformationMessage('BWContest: Repo cloned!');
return true;
}
function openRepoWorker(contestId: number, teamId: number): boolean {
const repoPaths = getRepoPaths(contestId, teamId);
if (!repoPaths.success) {
vscode.window.showErrorMessage('BWContest: BWContestSettings not configured');
return false;
}
const { clonedRepoPath } = repoPaths;
if (!fs.existsSync(clonedRepoPath)) {
vscode.window.showErrorMessage('BWContest: Local repo not found, clone first.');
return false;
}
if (vscode.workspace.workspaceFolders) {
const existingOpenFolderForRepo = vscode.workspace.workspaceFolders.filter((f) => {
const p =
os.platform() === 'win32'
? path.normalize(f.uri.path.slice(1))
: path.normalize(f.uri.path);
return p === path.normalize(clonedRepoPath);
})[0];
if (existingOpenFolderForRepo) {
vscode.window.showInformationMessage('BWContest: Repo is already opened');
return false;
}
}
outputPanelLog.trace(`Opening local repo from '${clonedRepoPath}'`);
const workspaceUpdateValid = vscode.workspace.updateWorkspaceFolders(
0,
vscode.workspace.workspaceFolders?.length ?? 0,
{ uri: vscode.Uri.file(clonedRepoPath), name: 'BWContest' }
);
if (!workspaceUpdateValid) {
vscode.window.showErrorMessage('BWContest: Request to open repository in VSCode failed');
return false;
}
// Shouldn't happen, or if it does the IDE will restart shortly after
vscode.window.showInformationMessage('BWContest: Team repository opened!');
return true;
}
export function getRepoPaths(
contestId: number,
teamId: number
): { success: false } | { success: true; repoUrl: string; clonedRepoPath: string } {
const currentSettings = vscode.workspace.getConfiguration().get<BWContestSettings>('BWContest');
if (!currentSettings || currentSettings.repoBaseUrl == '') {
// vscode.window.showErrorMessage('BWContest: BWContest.repoBaseURL not set');
return { success: false };
}
if (!currentSettings || currentSettings.repoClonePath == '') {
// vscode.window.showErrorMessage('BWContest: BWContest.repoClonePath not set');
return { success: false };
}
if (!currentSettings || currentSettings.webUrl == '') {
// vscode.window.showErrorMessage('BWContest: BWContest.webUrl not set');
return { success: false };
}
return {
success: true,
repoUrl: urlJoin(currentSettings.repoBaseUrl, contestId.toString(), `${teamId.toString()}.git`),
clonedRepoPath: path.join(
currentSettings.repoClonePath,
'BWContest',
contestId.toString(),
teamId.toString()
)
};
}
let repoManagerExtensionContext: vscode.ExtensionContext | null;
export function setRepoManagerExtensionContext(context: vscode.ExtensionContext) {
repoManagerExtensionContext = context;
}

View File

@ -7,6 +7,7 @@
MessageType, MessageType,
SidebarTeamStatus SidebarTeamStatus
} from '../../src/SidebarProvider'; } from '../../src/SidebarProvider';
import type { RepoState } from '../../src/teamRepoManager';
let teamname: string; let teamname: string;
let password: string; let password: string;
@ -16,16 +17,36 @@
let teamData: TeamData | null = null; let teamData: TeamData | null = null;
let teamStatus: SidebarTeamStatus | null = null; let teamStatus: SidebarTeamStatus | null = null;
let repoState: RepoState | null = null;
let totalProblems = 0; let totalProblems = 0;
function postMessage(message: MessageType) { function postMessage(message: MessageType) {
vscode.postMessage(message); vscode.postMessage(message);
} }
function onClone() { function onCloneOpenRepo() {
if (teamData) { if (teamData) {
postMessage({ postMessage({
msg: 'onClone', msg: 'onCloneOpenRepo',
data: { contestId: teamData.contestId, teamId: teamData.teamId }
});
}
}
function onCloneRepo() {
if (teamData) {
postMessage({
msg: 'onCloneRepo',
data: { contestId: teamData.contestId, teamId: teamData.teamId }
});
}
}
function onOpenRepo() {
if (teamData) {
postMessage({
msg: 'onOpenRepo',
data: { contestId: teamData.contestId, teamId: teamData.teamId } data: { contestId: teamData.contestId, teamId: teamData.teamId }
}); });
} }
@ -71,6 +92,8 @@
teamStatus.incorrectProblems.length + teamStatus.incorrectProblems.length +
teamStatus.notStartedProblems.length teamStatus.notStartedProblems.length
: 0; : 0;
} else if (m.msg === 'repoStateUpdated') {
repoState = m.data;
} }
}); });
</script> </script>
@ -111,10 +134,24 @@
<h2 class="sidebarSectionHeader">Actions</h2> <h2 class="sidebarSectionHeader">Actions</h2>
<div class="sidebarSection"> <div class="sidebarSection">
<div class="buttonContainer"> {#if repoState == 'No Team'}
<button on:click={onClone} class="sidebarButton">Clone and Open Repo</button> <span>Team not connected, click Refresh at the top of this panel</span>
<button on:click={onTestAndSubmit} class="sidebarButton">Test & Submit</button> {:else if repoState == 'No Repo'}
</div> <div class="buttonContainer">
<button on:click={onCloneOpenRepo} class="sidebarButton">Clone and Open Repo</button>
</div>
{:else if repoState == 'Repo Exists, Not Open'}
<div class="buttonContainer">
<button on:click={onOpenRepo} class="sidebarButton">Open Repo</button>
</div>
{:else if repoState == 'Repo Open'}
<div class="buttonContainer">
<button on:click={onTestAndSubmit} class="sidebarButton">Test & Submit</button>
<button on:click={onCloneRepo} class="sidebarButton">Reset Repo</button>
</div>
{:else}
<span>Checking repo state...</span>
{/if}
</div> </div>
<h2 class="sidebarSectionHeader">Problem Progress</h2> <h2 class="sidebarSectionHeader">Problem Progress</h2>