Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b30534f
feat(client): add PROMPT workspace integration with autosave
Geoshua Apr 22, 2026
56846ae
refactor(client): consolidate Save Teams as the single PROMPT-publish…
Geoshua Apr 23, 2026
f4b6e06
fix(client): set active course phase before hydrating workspace
Geoshua Apr 23, 2026
f33574c
docs(client): add JSDoc to public members + drop stale connection guard
Geoshua Apr 23, 2026
6c7900d
fix(client): clear cached course phases on unauthenticated probe
Geoshua Apr 23, 2026
80cc966
fix(client): don't drop edits made during in-flight workspace saves
Geoshua Apr 23, 2026
2fc28ba
fix(client): surface autosave failures + clear data on workspace reset
Geoshua Apr 23, 2026
47c9ef3
Merge branch 'main' into feat/automated-prompt-tease-data-exchange
Geoshua Apr 24, 2026
43a037a
refactor(client): drop project switcher, relabel header to allocations
Geoshua Apr 24, 2026
8962b92
fix(client): harden saveToPrompt + lowercase currentcolor keyword
Geoshua Apr 24, 2026
f8da024
fix(client): close silent-broadcast holes in distribute collaboration
Geoshua Apr 23, 2026
7cc661f
fix(client): route reconnect rebind through discover + diff overlay
Geoshua Apr 24, 2026
48903a4
Merge pull request #287 from prompt-edu/fix/collaboration-distribute-…
Geoshua Apr 24, 2026
bf6b1e8
feat(client): add global participant search to the header
Geoshua Apr 23, 2026
6e39fd2
fix(global-search): trim resting width + expand on focus
Geoshua Apr 24, 2026
73703ed
refactor(client): regroup workspace + collab status under project name
Geoshua Apr 24, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* tslint:disable */
/* eslint-disable */
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { StrictHttpResponse } from '../../strict-http-response';
import { RequestBuilder } from '../../request-builder';

import { TeaseSaveRequest, TeaseWorkspace } from '../../models/tease-workspace';

export interface TeaseCoursePhaseCoursePhaseIdSavePost$Params {
/**
* Unique identifier of the course phase
*/
coursePhaseId: string;
body?: TeaseSaveRequest;
}

/**
* Publish the Tease workspace + finalised allocations to PROMPT in a single
* atomic transaction via `POST /tease/course_phase/{coursePhaseId}/save`.
* Returns the server-stamped workspace (with updated `lastExportedAt`).
*/
export function teaseCoursePhaseCoursePhaseIdSavePost(
http: HttpClient,
rootUrl: string,
params: TeaseCoursePhaseCoursePhaseIdSavePost$Params,
context?: HttpContext
): Observable<StrictHttpResponse<TeaseWorkspace>> {
const rb = new RequestBuilder(rootUrl, teaseCoursePhaseCoursePhaseIdSavePost.PATH, 'post');
if (params) {
rb.path('coursePhaseId', params.coursePhaseId, {});
rb.body(params.body, 'application/json');
}

return http.request(rb.build({ responseType: 'json', accept: 'application/json', context })).pipe(
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
map((r: HttpResponse<any>) => {
return r as StrictHttpResponse<TeaseWorkspace>;
})
);
}

teaseCoursePhaseCoursePhaseIdSavePost.PATH = '/tease/course_phase/{coursePhaseId}/save';
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* tslint:disable */
/* eslint-disable */
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { StrictHttpResponse } from '../../strict-http-response';
import { RequestBuilder } from '../../request-builder';

import { TeaseWorkspace } from '../../models/tease-workspace';

export interface TeaseCoursePhaseCoursePhaseIdWorkspaceGet$Params {
/**
* Unique identifier of the course phase
*/
coursePhaseId: string;
}

/**
* Fetch the persisted Tease workspace for a course phase via
* `GET /tease/course_phase/{coursePhaseId}/workspace`. Returns an empty
* default workspace when the course phase has no saved state yet.
*/
export function teaseCoursePhaseCoursePhaseIdWorkspaceGet(
http: HttpClient,
rootUrl: string,
params: TeaseCoursePhaseCoursePhaseIdWorkspaceGet$Params,
context?: HttpContext
): Observable<StrictHttpResponse<TeaseWorkspace>> {
const rb = new RequestBuilder(rootUrl, teaseCoursePhaseCoursePhaseIdWorkspaceGet.PATH, 'get');
if (params) {
rb.path('coursePhaseId', params.coursePhaseId, {});
}

return http.request(rb.build({ responseType: 'json', accept: 'application/json', context })).pipe(
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
map((r: HttpResponse<any>) => {
return r as StrictHttpResponse<TeaseWorkspace>;
})
);
}

teaseCoursePhaseCoursePhaseIdWorkspaceGet.PATH = '/tease/course_phase/{coursePhaseId}/workspace';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* tslint:disable */
/* eslint-disable */
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { StrictHttpResponse } from '../../strict-http-response';
import { RequestBuilder } from '../../request-builder';

import { TeaseWorkspace, TeaseWorkspaceUpsert } from '../../models/tease-workspace';

export interface TeaseCoursePhaseCoursePhaseIdWorkspacePut$Params {
/**
* Unique identifier of the course phase
*/
coursePhaseId: string;
body?: TeaseWorkspaceUpsert;
}

/**
* Upsert the Tease workspace draft for a course phase via
* `PUT /tease/course_phase/{coursePhaseId}/workspace`. Idempotent — used
* by the client-side autosave loop. Does not touch the allocations table.
*/
export function teaseCoursePhaseCoursePhaseIdWorkspacePut(
http: HttpClient,
rootUrl: string,
params: TeaseCoursePhaseCoursePhaseIdWorkspacePut$Params,
context?: HttpContext
): Observable<StrictHttpResponse<TeaseWorkspace>> {
const rb = new RequestBuilder(rootUrl, teaseCoursePhaseCoursePhaseIdWorkspacePut.PATH, 'put');
if (params) {
rb.path('coursePhaseId', params.coursePhaseId, {});
rb.body(params.body, 'application/json');
}

return http.request(rb.build({ responseType: 'json', accept: 'application/json', context })).pipe(
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
map((r: HttpResponse<any>) => {
return r as StrictHttpResponse<TeaseWorkspace>;
})
);
}

teaseCoursePhaseCoursePhaseIdWorkspacePut.PATH = '/tease/course_phase/{coursePhaseId}/workspace';
38 changes: 38 additions & 0 deletions client/src/app/api/models/tease-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* tslint:disable */
/* eslint-disable */
import { Allocation } from './allocation';
import { ConstraintWrapper } from '../../shared/matching/constraints/constraint';

/**
* Persisted Tease workspace for a course phase, stored by PROMPT in
* team_allocation.tease_workspace.
*/
export interface TeaseWorkspace {
coursePhaseId: string;
constraints: ConstraintWrapper[];
/** [studentId, projectId] pairs */
lockedStudents: Array<[string, string]>;
allocationsDraft: Allocation[];
algorithmType?: 'preferenceMaxLP' | 'constraintOnly' | null;
lastSavedAt?: string | null;
lastExportedAt?: string | null;
updatedBy?: string | null;
}

/**
* Request body for PUT /tease/course_phase/{id}/workspace.
* Excludes server-managed timestamps.
*/
export type TeaseWorkspaceUpsert = Omit<
TeaseWorkspace,
'lastSavedAt' | 'lastExportedAt' | 'updatedBy'
>;

/**
* Request body for POST /tease/course_phase/{id}/save — the workspace
* payload plus the finalised allocations that should be upserted into
* the allocations table as part of the same DB transaction.
*/
export interface TeaseSaveRequest extends TeaseWorkspaceUpsert {
allocations: Allocation[];
}
8 changes: 6 additions & 2 deletions client/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
@if (dataLoaded) {
@if (mode === 'picker') {
<app-project-picker (coursePhaseSelected)="onCoursePhaseSelected($event)"></app-project-picker>
} @else if (dataLoaded) {
<div class="bg-bright h-100 d-flex flex-column">
<app-navigation-bar [allocationData]="allocationData"></app-navigation-bar>
<app-navigation-bar
[allocationData]="allocationData"
(coursePhaseSelected)="onCoursePhaseSelected($event)"></app-navigation-bar>
<app-projects class="flex-grow-1 overflow-hidden" [projectsData]="allocationData.projectsData"></app-projects>
<div
class="resizable"
Expand Down
Loading
Loading