Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.14.1
10 changes: 10 additions & 0 deletions LICENSE-THIRD-PARTY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Third Party Licenses
====================

While Submitty VSCode is made available under the [BSD "3-Clause" License](https://github.com/Submitty/Submitty-VSCode/blob/master/LICENSE.md)
we utilize several third-party libraries to help power various components. Below is a list of these components and their relevant copyrights, copied when the source was included/last updated within Submitty.


| Component | Copyright | Url | License |
|--------------|----------------------------------------------------------------------|-----|---------|
| vscode-git | Copyright (c) Microsoft Corporation. All rights reserved. | https://code.visualstudio.com | [MIT License](https://github.com/microsoft/vscode/blob/main/LICENSE.txt) |
2,117 changes: 1,004 additions & 1,113 deletions package-lock.json

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
{
"type": "webview",
"id": "submittyWebview",
"name": "Sidebar",
"name": "Submitty",
"icon": "media/duck.png"
}
]
Expand All @@ -75,22 +75,22 @@
"test": "vscode-test"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/mocha": "^10.0.7",
"@types/node": "20.x",
"@types/vscode": "^1.95.0",
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"eslint": "^9.39.1",
"eslint-config-prettier": "^9.1.0",
"globals": "^16.5.0",
"prettier": "^3.3.3",
"rimraf": "^3.0.2",
"typescript": "^5.4.5",
"typescript-eslint": "^8.46.3"
"@eslint/js": "10.0.1",
"@types/mocha": "^10.0.10",
"@types/node": "^24.12.2",
"@types/vscode": "^1.118.0",
"@vscode/test-cli": "^0.0.11",
"@vscode/test-electron": "^2.5.2",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.6.0",
"prettier": "^3.8.3",
"rimraf": "^6.1.3",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.1"
},
"dependencies": {
"axios": "^1.7.8",
"axios": "^1.15.2",
"keytar": "^7.9.0"
}
}
48 changes: 46 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,62 @@ import * as vscode from 'vscode';
import { SidebarProvider } from './sidebarProvider';
import { ApiService } from './services/apiService';
import { TestingService } from './services/testingService';
import { GitService } from './services/gitService';
import { AuthService } from './services/authService';
import { CourseRepoResolver } from './services/courseRepoResolver';

export function activate(context: vscode.ExtensionContext): void {
const apiService = ApiService.getInstance(context, '');
const testingService = new TestingService(context, apiService);
const sidebarProvider = new SidebarProvider(context, testingService);
const gitService = new GitService();
const authService = AuthService.getInstance(context);
const sidebarProvider = new SidebarProvider(
context,
testingService,
gitService
);

context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
'submittyWebview',
sidebarProvider
)
);

// Preload gradables into the Test Explorer when the workspace appears
// to be a course-tied repo.
void (async () => {
try {
await authService.initialize();
const resolver = new CourseRepoResolver(
apiService,
authService,
gitService
);
const courseContext = await resolver.resolveCourseContextFromRepo();
if (!courseContext) {
return;
}

const gradablesResponse = await apiService.fetchGradables(
courseContext.courseId,
courseContext.term
);
const gradables = Object.values(gradablesResponse.data);

for (const g of gradables) {
testingService.addGradeable(
courseContext.term,
courseContext.courseId,
g.id,
g.title || g.id
);
}
} catch (e) {
const err = e instanceof Error ? e.message : String(e);
console.warn(`Failed to preload gradables: ${err}`);
}
})();
}

export function deactivate() {}
export function deactivate(): void {}
4 changes: 3 additions & 1 deletion src/interfaces/Responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export type LoginResponse = ApiResponse<{
token: string;
}>;

export type GradableResponse = ApiResponse<Gradable[]>;
export type GradableResponse = ApiResponse<{
[key: string]: Gradable;
}>;
74 changes: 48 additions & 26 deletions src/services/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@

import * as vscode from 'vscode';
import { ApiClient } from './apiClient';

import {
CourseResponse,
LoginResponse,
GradableResponse,
} from '../interfaces/Responses';
import { AutoGraderDetails } from '../interfaces/AutoGraderDetails';

function getErrorMessage(error: unknown, fallback: string): string {
if (error instanceof Error) {
return error.message || fallback;
}
if (typeof error === 'object' && error) {
const maybeAxiosError = error as {
response?: { data?: { message?: unknown } };
};
const msg = maybeAxiosError.response?.data?.message;
if (typeof msg === 'string' && msg.trim()) {
return msg;
}
}
return fallback;
}

export class ApiService {
private client: ApiClient;
private static instance: ApiService;
Expand All @@ -22,12 +37,12 @@ export class ApiService {
}

// set token for local api client
setAuthorizationToken(token: string) {
setAuthorizationToken(token: string): void {
this.client.setToken(token);
}

// set base URL for local api client
setBaseUrl(baseUrl: string) {
setBaseUrl(baseUrl: string): void {
this.client.setBaseURL(baseUrl);
}

Expand All @@ -49,34 +64,36 @@ export class ApiService {

const token: string = response.data.data.token;
return token;
} catch (error: any) {
throw new Error(
error.response?.data?.message || error.message || 'Login failed.'
);
} catch (error: unknown) {
throw new Error(getErrorMessage(error, 'Login failed.'), {
cause: error,
});
}
}

async fetchMe(): Promise<any> {
try {
const response = await this.client.get<any>('/api/me');
return response.data;
} catch (error: any) {
throw new Error(error.response?.data?.message || 'Failed to fetch me.');
} catch (error: unknown) {
throw new Error(getErrorMessage(error, 'Failed to fetch me.'), {
cause: error,
});
}
}

/**
* Fetch all courses for the authenticated user
*/
async fetchCourses(token?: string): Promise<CourseResponse> {
async fetchCourses(_token?: string): Promise<CourseResponse> {
try {
const response = await this.client.get<CourseResponse>('/api/courses');
return response.data;
} catch (error: any) {
} catch (error: unknown) {
console.error('Error fetching courses:', error);
throw new Error(
error.response?.data?.message || 'Failed to fetch courses.'
);
throw new Error(getErrorMessage(error, 'Failed to fetch courses.'), {
cause: error,
});
}
}

Expand All @@ -88,11 +105,11 @@ export class ApiService {
const url = `/api/${term}/${courseId}/gradeables`;
const response = await this.client.get<GradableResponse>(url);
return response.data;
} catch (error: any) {
} catch (error: unknown) {
console.error('Error fetching gradables:', error);
throw new Error(
error.response?.data?.message || 'Failed to fetch gradables.'
);
throw new Error(getErrorMessage(error, 'Failed to fetch gradables.'), {
cause: error,
});
}
}

Expand All @@ -109,10 +126,11 @@ export class ApiService {
`/api/${term}/${courseId}/gradeable/${gradeableId}/values`
);
return response.data;
} catch (error: any) {
} catch (error: unknown) {
console.error('Error fetching grade details:', error);
throw new Error(
error.response?.data?.message || 'Failed to fetch grade details.'
getErrorMessage(error, 'Failed to fetch grade details.'),
{ cause: error }
);
}
}
Expand Down Expand Up @@ -170,10 +188,13 @@ export class ApiService {
const url = `/api/${term}/${courseId}/gradeable/${gradeableId}/upload?vcs_upload=true&git_repo_id=true`;
const response = await this.client.post<any>(url);
return response.data;
} catch (error: any) {
} catch (error: unknown) {
console.error('Error submitting VCS gradable:', error);
throw new Error(
error.response?.data?.message || 'Failed to submit VCS gradable.'
getErrorMessage(error, 'Failed to submit VCS gradable.'),
{
cause: error,
}
);
}
}
Expand All @@ -185,15 +206,16 @@ export class ApiService {
term: string,
courseId: string,
gradeableId: string
): Promise<any[]> {
): Promise<unknown[]> {
try {
const url = `/api/${term}/${courseId}/gradeable/${gradeableId}/attempts`;
const response = await this.client.get<any>(url);
const response = await this.client.get<unknown[]>(url);
return response.data;
} catch (error: any) {
} catch (error: unknown) {
console.error('Error fetching previous attempts:', error);
throw new Error(
error.response?.data?.message || 'Failed to fetch previous attempts.'
getErrorMessage(error, 'Failed to fetch previous attempts.'),
{ cause: error }
);
}
}
Expand Down
Loading
Loading