From 838947e06daf2ba4b550589deb05ffacf0feeed9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 01:48:27 +0000 Subject: [PATCH 1/5] P4: Share AddClassForm between Add Class and Onboarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract flat enroll UI/logic into AddClassForm (enrollClass via store.add_class). Thin shells keep modal/welcome chrome only. Add parseJoinRef helper + CI unit tests for join-ref → bare classId. Co-authored-by: Sander Vonk --- .github/workflows/ci.yml | 3 + package.json | 1 + scripts/test-join-class.mjs | 82 ++++++ src/common/joinClass.ts | 49 ++++ src/components/Portal/AddClassForm.vue | 355 +++++++++++++++++++++++++ src/views/Portal/AddClass.vue | 276 +------------------ src/views/Portal/Onboarding.vue | 124 +-------- 7 files changed, 511 insertions(+), 379 deletions(-) create mode 100644 scripts/test-join-class.mjs create mode 100644 src/common/joinClass.ts create mode 100644 src/components/Portal/AddClassForm.vue diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8159a93..ac4d04dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,5 +46,8 @@ jobs: - name: Repeating edit-scope tests run: npm run test:repeating-scope + - name: Join-class ref tests + run: npm run test:join-class + - name: Build run: npm run build diff --git a/package.json b/package.json index 2c3d0b75..87f5bc1e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test:me-board": "node --experimental-strip-types scripts/test-me-board.mjs", "test:acting-as": "node --experimental-strip-types scripts/test-acting-as.mjs", "test:repeating-scope": "node --experimental-strip-types scripts/test-repeating-scope.mjs", + "test:join-class": "node --experimental-strip-types scripts/test-join-class.mjs", "deploy": "node deploy.js", "postbuild": "cross-env OS_TYPE=$(uname -s) npm-run-all --parallel copy-files echo-message", "copy-files": "npm run copy-win || npm run copy-nix", diff --git a/scripts/test-join-class.mjs b/scripts/test-join-class.mjs new file mode 100644 index 00000000..3cc75c77 --- /dev/null +++ b/scripts/test-join-class.mjs @@ -0,0 +1,82 @@ +/** + * Unit tests for P4 join-ref → bare classId resolution (Add Class / Onboarding). + * Run: node --experimental-strip-types scripts/test-join-class.mjs + */ +import { parseJoinRef } from "../src/common/joinClass.ts"; + +let failed = 0; + +function assert(cond, msg) { + if (!cond) { + failed++; + console.error("FAIL:", msg); + } else { + console.log("ok:", msg); + } +} + +function assertEq(actual, expected, label) { + const ok = JSON.stringify(actual) === JSON.stringify(expected); + if (!ok) { + failed++; + console.error( + `FAIL: ${label}\n expected: ${JSON.stringify(expected)}\n actual: ${JSON.stringify(actual)}` + ); + } else { + console.log(`ok: ${label}`); + } +} + +const org = "mvla.net"; +const orgAt = "@mvla.net"; + +console.log("--- parseJoinRef (P4 flat enroll) ---\n"); + +assertEq(parseJoinRef(null, org), null, "null → null"); +assertEq(parseJoinRef("", org), null, "empty → null"); +assertEq(parseJoinRef(" ", org), null, "whitespace → null"); + +assertEq( + parseJoinRef("abc123", org), + { classId: "abc123", needsTeacherLookup: true }, + "bare classId needs teacher dual-read" +); + +assertEq( + parseJoinRef("t@mvla.net/abc123", org), + { classId: "abc123", teacherEmail: "t@mvla.net", needsTeacherLookup: false }, + "email/classId → bare classId + teacher" +); + +assertEq( + parseJoinRef("t@mvla.net~abc123", org), + { classId: "abc123", teacherEmail: "t@mvla.net", needsTeacherLookup: false }, + "email~classId → bare classId + teacher" +); + +assertEq( + parseJoinRef("t~abc123", org), + { classId: "abc123", teacherEmail: "t@mvla.net", needsTeacherLookup: false }, + "local~classId expands org domain" +); + +assertEq( + parseJoinRef("t~abc123", orgAt), + { classId: "abc123", teacherEmail: "t@mvla.net", needsTeacherLookup: false }, + "local~classId with @orgDomain store form" +); + +assertEq( + parseJoinRef(" teacher@mvla.net/cls9 ", org), + { classId: "cls9", teacherEmail: "teacher@mvla.net", needsTeacherLookup: false }, + "trims whitespace around join ref" +); + +assert(parseJoinRef("only-id", org)?.needsTeacherLookup === true, "short ref flags lookup"); +assert(parseJoinRef("a@b.c/id", org)?.needsTeacherLookup === false, "prefixed ref skips lookup"); + +if (failed) { + console.error(`\n${failed} failure(s)`); + process.exit(1); +} +console.log("\nall join-class tests passed"); diff --git a/src/common/joinClass.ts b/src/common/joinClass.ts new file mode 100644 index 00000000..9eb35d9b --- /dev/null +++ b/src/common/joinClass.ts @@ -0,0 +1,49 @@ +/** + * P4 flat enroll helpers for Add Class / Onboarding join-ref resolution. + * Prefer bare classId enrollment pointers; dual-read still resolves legacy + * email/classId and local~classId forms during soak. + * + * @module common/joinClass + */ + +import { parseClassId } from "./paths"; + +export interface JoinRefParts { + /** Bare classId for enrollClass({ classId }) / store.add_class. */ + classId: string; + /** Present when the join ref included a teacher email or local prefix. */ + teacherEmail?: string; + /** + * True for short bare classId refs — caller must dual-read (class_from_ref) + * to obtain teacher email before listing classes. + */ + needsTeacherLookup: boolean; +} + +/** + * Parse a join ref (from route or code→ref lookup) into teacher + bare classId. + * Does not touch Firestore; async teacher lookup stays in the Vue form. + * + * Accepts: classId | email/classId | local~classId (slash or ~). + */ +export function parseJoinRef( + ref: string | undefined | null, + orgDomain: string +): JoinRefParts | null { + if (!ref || typeof ref !== "string") return null; + const trimmed = ref.trim(); + if (!trimmed) return null; + + const parsed = parseClassId(trimmed, orgDomain); + if (!parsed?.classId) return null; + + if (!parsed.hasTeacherPrefix) { + return { classId: parsed.classId, needsTeacherLookup: true }; + } + + return { + classId: parsed.classId, + teacherEmail: parsed.teacherEmail, + needsTeacherLookup: false, + }; +} diff --git a/src/components/Portal/AddClassForm.vue b/src/components/Portal/AddClassForm.vue new file mode 100644 index 00000000..137ff6b0 --- /dev/null +++ b/src/components/Portal/AddClassForm.vue @@ -0,0 +1,355 @@ + + + + + diff --git a/src/views/Portal/AddClass.vue b/src/views/Portal/AddClass.vue index 954a48ed..4dbb5565 100644 --- a/src/views/Portal/AddClass.vue +++ b/src/views/Portal/AddClass.vue @@ -3,289 +3,31 @@ -
-
- {{ $store.loaded_email == teacher_email ? "Loaded" : "Loading" }} from your teacher's - class {{ code ? "code" : "ref" }} - Join a class with your teacher's details or - enter a class code - -
-
- - -
-
-
- {{ code || $route.params.ref || "" }} -
-
-
- {{ - class_obj && class_obj.is_joined && !adding - ? "You've already joined" - : "You'll be joining" - }} - {{ $store.class_text(class_obj) }} -
-
-
-
-
- If you're having trouble, ask your teacher to share their class code again using the - Share button on the class edit page - - Enter your teacher's email above to see their classes -
-
-
- -
- -
+ + - + diff --git a/src/views/Portal/Onboarding.vue b/src/views/Portal/Onboarding.vue index faf8223d..767d52d5 100644 --- a/src/views/Portal/Onboarding.vue +++ b/src/views/Portal/Onboarding.vue @@ -3,130 +3,30 @@ -
-
{{ name }} Let's get you set up with your first class
-
- - -
-
- You can add more classes later from the - Join a Class button on the left sidebar -
-
-
- -
- -
+ + From 98a6c3c5e5630712bf72f5dd60ff02aaf196eba2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 01:49:54 +0000 Subject: [PATCH 2/5] Fix parseJoinRef: colocate in paths.ts for Node tests Relative TS imports break under --experimental-strip-types; keep join-ref helper in the existing leaf paths module. Co-authored-by: Sander Vonk --- .env | 62 ++++++++++++++++++++++++++ scripts/test-join-class.mjs | 2 +- src/common/joinClass.ts | 49 -------------------- src/common/paths.ts | 44 ++++++++++++++++++ src/components/Portal/AddClassForm.vue | 2 +- 5 files changed, 108 insertions(+), 51 deletions(-) create mode 100644 .env delete mode 100644 src/common/joinClass.ts diff --git a/.env b/.env new file mode 100644 index 00000000..07363569 --- /dev/null +++ b/.env @@ -0,0 +1,62 @@ +# the main brand name shown in files +VUE_APP_BRAND_NAME_LONG=Task Tracker [BETA] +# the short name used in the app +VUE_APP_BRAND_NAME_SHORT=[BETA] +# the main brand URL (without trailing slash) +VUE_APP_BRAND_URL=http://localhost:8080 +# the main brand homepage used for pwa, etc (without trailing slash) +VUE_APP_BRAND_HOMEPAGE=http://localhost:8080 +# the main brand domain (without subdomain) +VUE_APP_BRAND_DOMAIN=localhost:8080 +# excluding the hash due to limitations in the .env file +VUE_APP_THEME_COLOR=f5c14b +VUE_APP_THEME_ACCENT_COLOR=000000 +VUE_APP_THEME_ACCENT_COLOR_ALT=4c4c4c +# the URL to download the sidebar +VUE_APP_SIDEBAR_DOWNLOAD_URL=/download/mvtt-sidebar.zip + +# the domain for the API +VUE_APP_BRAND_DOMAIN__API=api.mvtt.app +# the domain that redirects to the class add view +VUE_APP_BRAND_DOMAIN__ADDCLASS=localhost:8080/add +# the domain that redirects to the class view +VUE_APP_BRAND_DOMAIN__VIEWCLASS=localhost:8080/view +# the domain that redirects to the task view +VUE_APP_BRAND_DOMAIN__VIEWTASK=localhost:8080/view +# the main mail address for the brand (used to send the invite mail) +VUE_APP_BRAND_MAIL_ADDRESS=mail@mvtt.app + +# allowed domain hosts w/o outlink protection (either literal or *.domain.tld for wildcard) +VUE_APP_BRAND_ALLOWED_HOSTS=mvla.net,*.mvla.net + +# console colors +VUE_APP_THEME_CONSOLE_COLOR_BG=272727 +VUE_APP_THEME_CONSOLE_COLOR_TEXT=C9B092 + +# the organization domain for user emails +VUE_APP_ORG_DOMAIN=mvla.net +# the organization (short) name +VUE_APP_ORG_NAME=SCHOOL +# links to relevant sites to be included in the basenav +VUE_APP_ORG_LINKS=[["Docs", "https://localhost:8080/docs"]] +# grade levels and their names +VUE_APP_ORG_GRADES=[["student", "Student"]] +# name for teacher acc +VUE_APP_ORG_TEACHER_GRADE=Teacher + +# the API key for the Firebase project +FIREBASE_apiKey=AIzaSyDGqqhApln4pxm4EynPCSkfwyK8aopjfCk +# the domain for the Firebase auth +FIREBASE_authDomain=auth.mvtt.app +# the project ID of the Firebase project +FIREBASE_projectId=mvtrack +# the storage bucket for the Firebase project +FIREBASE_storageBucket=mvtrack.appspot.com +# the messaging sender ID for the Firebase project +FIREBASE_messagingSenderId=1070634963357 +# the app ID for the Firebase project +FIREBASE_appId=1:1070634963357:web:2a4abc725fff24a5199f74 +# the measurement ID for the Firebase project +FIREBASE_measurementId=G-6LNX1KCR8E +# the server region for the Firebase project +FIREBASE_serverRegion=us-central1 \ No newline at end of file diff --git a/scripts/test-join-class.mjs b/scripts/test-join-class.mjs index 3cc75c77..77e0ebdc 100644 --- a/scripts/test-join-class.mjs +++ b/scripts/test-join-class.mjs @@ -2,7 +2,7 @@ * Unit tests for P4 join-ref → bare classId resolution (Add Class / Onboarding). * Run: node --experimental-strip-types scripts/test-join-class.mjs */ -import { parseJoinRef } from "../src/common/joinClass.ts"; +import { parseJoinRef } from "../src/common/paths.ts"; let failed = 0; diff --git a/src/common/joinClass.ts b/src/common/joinClass.ts deleted file mode 100644 index 9eb35d9b..00000000 --- a/src/common/joinClass.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * P4 flat enroll helpers for Add Class / Onboarding join-ref resolution. - * Prefer bare classId enrollment pointers; dual-read still resolves legacy - * email/classId and local~classId forms during soak. - * - * @module common/joinClass - */ - -import { parseClassId } from "./paths"; - -export interface JoinRefParts { - /** Bare classId for enrollClass({ classId }) / store.add_class. */ - classId: string; - /** Present when the join ref included a teacher email or local prefix. */ - teacherEmail?: string; - /** - * True for short bare classId refs — caller must dual-read (class_from_ref) - * to obtain teacher email before listing classes. - */ - needsTeacherLookup: boolean; -} - -/** - * Parse a join ref (from route or code→ref lookup) into teacher + bare classId. - * Does not touch Firestore; async teacher lookup stays in the Vue form. - * - * Accepts: classId | email/classId | local~classId (slash or ~). - */ -export function parseJoinRef( - ref: string | undefined | null, - orgDomain: string -): JoinRefParts | null { - if (!ref || typeof ref !== "string") return null; - const trimmed = ref.trim(); - if (!trimmed) return null; - - const parsed = parseClassId(trimmed, orgDomain); - if (!parsed?.classId) return null; - - if (!parsed.hasTeacherPrefix) { - return { classId: parsed.classId, needsTeacherLookup: true }; - } - - return { - classId: parsed.classId, - teacherEmail: parsed.teacherEmail, - needsTeacherLookup: false, - }; -} diff --git a/src/common/paths.ts b/src/common/paths.ts index d8c73798..f6764244 100644 --- a/src/common/paths.ts +++ b/src/common/paths.ts @@ -22,6 +22,22 @@ export interface ParsedClassPath { hasTeacherPrefix: boolean; } +/** + * P4 flat enroll: join-ref parts for Add Class / Onboarding. + * Prefer bare classId for enrollClass; dual-read when teacher is unknown. + */ +export interface JoinRefParts { + /** Bare classId for enrollClass({ classId }) / store.add_class. */ + classId: string; + /** Present when the join ref included a teacher email or local prefix. */ + teacherEmail?: string; + /** + * True for short bare classId refs — caller must dual-read (class_from_ref) + * to obtain teacher email before listing classes. + */ + needsTeacherLookup: boolean; +} + export interface ParsedTaskPath { teacherEmail?: string; classId: string; @@ -86,6 +102,34 @@ export function parseClassId( return null; } +/** + * P4: Parse a join ref (route or code→ref) into teacher + bare classId. + * Does not touch Firestore; async teacher lookup stays in AddClassForm. + * Accepts: classId | email/classId | local~classId (slash or ~). + * No nested classes/{email}/classes writers — bare classId enroll only. + */ +export function parseJoinRef( + ref: string | undefined | null, + orgDomain: string +): JoinRefParts | null { + if (!ref || typeof ref !== "string") return null; + const trimmed = ref.trim(); + if (!trimmed) return null; + + const parsed = parseClassId(trimmed, orgDomain); + if (!parsed?.classId) return null; + + if (!parsed.hasTeacherPrefix) { + return { classId: parsed.classId, needsTeacherLookup: true }; + } + + return { + classId: parsed.classId, + teacherEmail: parsed.teacherEmail, + needsTeacherLookup: false, + }; +} + /** * Parse task identity from slash id or ~ ref. * Accepts: classId/taskId | email/classId/taskId | local~classId~taskId | classId~taskId diff --git a/src/components/Portal/AddClassForm.vue b/src/components/Portal/AddClassForm.vue index 137ff6b0..e7ba0c73 100644 --- a/src/components/Portal/AddClassForm.vue +++ b/src/components/Portal/AddClassForm.vue @@ -156,7 +156,7 @@ */ import { ErrorToast, WarningToast } from "@svonk/util"; import smoothReflow from "vue-smooth-reflow"; -import { parseJoinRef } from "@/common/joinClass"; +import { parseJoinRef } from "@/common/paths"; export default { name: "AddClassForm", From 46bb14ac199369a0eb487b1855408d60ae844c71 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 01:50:06 +0000 Subject: [PATCH 3/5] Stop tracking accidental .env; ignore local .env copies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI already copies .env.test → .env; do not commit local env. Co-authored-by: Sander Vonk --- .env | 62 ------------------------------------------------------------ 1 file changed, 62 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 07363569..00000000 --- a/.env +++ /dev/null @@ -1,62 +0,0 @@ -# the main brand name shown in files -VUE_APP_BRAND_NAME_LONG=Task Tracker [BETA] -# the short name used in the app -VUE_APP_BRAND_NAME_SHORT=[BETA] -# the main brand URL (without trailing slash) -VUE_APP_BRAND_URL=http://localhost:8080 -# the main brand homepage used for pwa, etc (without trailing slash) -VUE_APP_BRAND_HOMEPAGE=http://localhost:8080 -# the main brand domain (without subdomain) -VUE_APP_BRAND_DOMAIN=localhost:8080 -# excluding the hash due to limitations in the .env file -VUE_APP_THEME_COLOR=f5c14b -VUE_APP_THEME_ACCENT_COLOR=000000 -VUE_APP_THEME_ACCENT_COLOR_ALT=4c4c4c -# the URL to download the sidebar -VUE_APP_SIDEBAR_DOWNLOAD_URL=/download/mvtt-sidebar.zip - -# the domain for the API -VUE_APP_BRAND_DOMAIN__API=api.mvtt.app -# the domain that redirects to the class add view -VUE_APP_BRAND_DOMAIN__ADDCLASS=localhost:8080/add -# the domain that redirects to the class view -VUE_APP_BRAND_DOMAIN__VIEWCLASS=localhost:8080/view -# the domain that redirects to the task view -VUE_APP_BRAND_DOMAIN__VIEWTASK=localhost:8080/view -# the main mail address for the brand (used to send the invite mail) -VUE_APP_BRAND_MAIL_ADDRESS=mail@mvtt.app - -# allowed domain hosts w/o outlink protection (either literal or *.domain.tld for wildcard) -VUE_APP_BRAND_ALLOWED_HOSTS=mvla.net,*.mvla.net - -# console colors -VUE_APP_THEME_CONSOLE_COLOR_BG=272727 -VUE_APP_THEME_CONSOLE_COLOR_TEXT=C9B092 - -# the organization domain for user emails -VUE_APP_ORG_DOMAIN=mvla.net -# the organization (short) name -VUE_APP_ORG_NAME=SCHOOL -# links to relevant sites to be included in the basenav -VUE_APP_ORG_LINKS=[["Docs", "https://localhost:8080/docs"]] -# grade levels and their names -VUE_APP_ORG_GRADES=[["student", "Student"]] -# name for teacher acc -VUE_APP_ORG_TEACHER_GRADE=Teacher - -# the API key for the Firebase project -FIREBASE_apiKey=AIzaSyDGqqhApln4pxm4EynPCSkfwyK8aopjfCk -# the domain for the Firebase auth -FIREBASE_authDomain=auth.mvtt.app -# the project ID of the Firebase project -FIREBASE_projectId=mvtrack -# the storage bucket for the Firebase project -FIREBASE_storageBucket=mvtrack.appspot.com -# the messaging sender ID for the Firebase project -FIREBASE_messagingSenderId=1070634963357 -# the app ID for the Firebase project -FIREBASE_appId=1:1070634963357:web:2a4abc725fff24a5199f74 -# the measurement ID for the Firebase project -FIREBASE_measurementId=G-6LNX1KCR8E -# the server region for the Firebase project -FIREBASE_serverRegion=us-central1 \ No newline at end of file From 908b0088307f3ecf9ba6705dce80e95ec28fd3aa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 01:50:12 +0000 Subject: [PATCH 4/5] Ignore local .env files (CI copies from .env.test) Co-authored-by: Sander Vonk --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 448815d5..0f8167e6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules /server # local env files +.env .env.local .env.*.local From 01b551dd7ec6c9e5a53eadd5683eb08096552a31 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 01:50:32 +0000 Subject: [PATCH 5/5] Clarify join-class test import comment Co-authored-by: Sander Vonk --- scripts/test-join-class.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/test-join-class.mjs b/scripts/test-join-class.mjs index 77e0ebdc..1adaae77 100644 --- a/scripts/test-join-class.mjs +++ b/scripts/test-join-class.mjs @@ -1,6 +1,7 @@ /** * Unit tests for P4 join-ref → bare classId resolution (Add Class / Onboarding). * Run: node --experimental-strip-types scripts/test-join-class.mjs + * (imports parseJoinRef from ../src/common/paths.ts) */ import { parseJoinRef } from "../src/common/paths.ts";