Skip to content

Bug-3566: Add searchable project group picker and TDEI fixes#47

Merged
jeffmaki merged 3 commits intomasterfrom
bugfix-3566-workspace-select-pg
Apr 29, 2026
Merged

Bug-3566: Add searchable project group picker and TDEI fixes#47
jeffmaki merged 3 commits intomasterfrom
bugfix-3566-workspace-select-pg

Conversation

@shweta2101
Copy link
Copy Markdown
Contributor

@shweta2101 shweta2101 commented Apr 28, 2026

DevBoard Task

https://dev.azure.com/TDEI-UW/TDEI/_workitems/edit/3566

Changes Implemented:

  1. Replace simple select with a searchable, keyboard-navigable ProjectGroupPicker (infinite scroll, debounced search, click-outside handling, highlighting).
  2. Add scoped styles and accessibility improvements.
  3. Update services/tdei.ts with stronger TypeScript annotations, Promise return types, optional chaining, safer JWT field casts, and paginated/searchable getMyProjectGroups API. Apply related type and runtime guards in pages/workspace/create/tdei.vue (typed refs, map removal guard, bounds check, and safer import parameter casts).
  4. Also add "qrcode" to package.json dependencies. Added as it lists qrcode as a peer dependency and threw errors while running.

Impacted Areas For Testing:

Project Group Picker

Page URL
Dashboard /dashboard
Create Workspace (TDEI) /workspace/create/tdei
Create Workspace (File) /workspace/create/file
Create Workspace (Blank) /workspace/create/blank
Export Workspace (TDEI) /workspace/[id]/export/tdei

Test Scenarios

  • Clicking the field opens the dropdown
  • Typing in the search box filters results (debounced ~300ms)
  • Scrolling to the bottom loads the next 10 items (lazy loading)
  • Up/Down arrow keys navigate the list, Enter selects
  • Clicking an item selects it and closes the dropdown
  • Selected group name persists in the input field when focused again
  • Clicking outside closes the dropdown without changing selection

Screenshots:

Screenshot 2026-04-28 at 6 31 42 PM Screenshot 2026-04-28 at 6 31 56 PM Screenshot 2026-04-28 at 6 32 07 PM

Overview

This PR replaces the simple project-group with a searchable, keyboard-navigable ProjectGroupPicker and tightens TDEI-related TypeScript and runtime guards. It also fixes a missing runtime dependency by adding "qrcode". Changes by File components/ProjectGroupPicker.vue Replaces fully-fetched with an async, searchable dropdown supporting:

  • Debounced text search (~300ms) and server-side pagination (infinite scroll / lazy load)
  • Keyboard navigation (Up/Down highlight, Enter select, Escape to cancel)
  • Click-to-select, click-outside to close/restore, item highlighting, loading & empty states
  • Scoped styles, accessibility improvements, disabled prop, and pendingReset logic for queued resets during loading
  • Initialization now loads groups on mount and syncs initial model into the input

services/tdei.ts

  • Stronger TypeScript annotations and runtime safety:
    • Explicit Promise return types for dataset/upload/download/convert methods
    • getJwtBody typed to return any and JWT fields validated/cast (string-or-empty fallback)
    • TdeiConversionError now stores typed job: any
    • openDatasetArchive gains optional-chaining guards and safer filename predicate typing
    • Upload methods require dataset: Blob and metadata: any
    • _send marked override
  • getMyProjectGroups expanded to accept pageNo, pageSize and searchText, builds query string, parses responses as TdeiProjectGroupApiResponse[] and returns [] on parse failure
  • getMyServices annotated to return Promise<TdeiService[]>

types/tdei.ts

  • Adds exported API response interfaces and normalized UI types:
    • TdeiProjectGroupApiResponse, TdeiServiceApiResponse, TdeiDatasetApiResponse
    • TdeiProjectGroup, TdeiService, TdeiDatasetSummary

pages/workspace/create/tdei.vue

  • Tighter types and safer runtime behavior:
    • Typed refs (tdeiRecordId/projectGroupId as string | null), explicit Leaflet global declaration
    • getDatasetInfo accepts string | null, resets state when id is null, guards against falsy API responses before merging via Object.assign
    • Removes existing Leaflet map instance before reinitializing; only fits bounds when present and valid
    • Safer workspace payload construction with casts for nullable IDs and optional-chaining fallback for tdeiServiceId

package.json

  • Adds runtime dependency "qrcode" (fixes prior runtime errors caused by it being only a peer dependency)

Test Coverage / QA Scenarios

  • Dropdown open/close behavior, debounced search filtering, lazy-loading on scroll (loads next 10 items), keyboard navigation (Up/Down/Enter/Escape), click-to-select and close, selection persistence when re-focusing, and click-outside closing without changing selection.

Impacted Pages

  • /dashboard
  • /workspace/create/tdei
  • /workspace/create/file
  • /workspace/create/blank
  • /workspace/[id]/export/tdei

Replace simple select with a searchable, keyboard-navigable ProjectGroupPicker (infinite scroll, debounced search, click-outside handling, highlighting). Add scoped styles and accessibility improvements. Update services/tdei.ts with stronger TypeScript annotations, Promise return types, optional chaining, safer JWT field casts, and paginated/searchable getMyProjectGroups API. Apply related type and runtime guards in pages/workspace/create/tdei.vue (typed refs, map removal guard, bounds check, and safer import parameter casts). Also add "qrcode" to package.json dependencies.
@shweta2101 shweta2101 requested review from jeffmaki and susrisha April 28, 2026 14:23
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Warning

Rate limit exceeded

@shweta2101 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 47 minutes and 58 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 17aecd6c-9522-49d0-b1fb-4a462130b675

📥 Commits

Reviewing files that changed from the base of the PR and between 4c3f55c and b67fe96.

📒 Files selected for processing (1)
  • pages/workspace/create/tdei.vue
📝 Walkthrough

Walkthrough

Replaces a static project-group select with a searchable, paginated dropdown (keyboard + infinite-scroll). Strengthens TypeScript typings for TDEI services and page code, adds new TDEI API response and UI types, improves map null-safety, and adds the qrcode dependency.

Changes

Cohort / File(s) Summary
Project Group Picker (UI)
components/ProjectGroupPicker.vue
Replaces full static select with an async, searchable dropdown: paginated loading, debounced search, loading state, open/close management, keyboard navigation (Arrow/Enter/Escape), item highlighting, Escape/outside-click restore, and infinite-scroll loading.
TDEI Service & Types
services/tdei.ts, types/tdei.ts
Adds explicit TypeScript interfaces for API responses and normalized UI types; tightens method signatures/return types, marks internal _send overrides, improves parsing/guards, expands getMyProjectGroups to accept pageNo/searchText/pageSize.
Workspace Page (map & payload)
pages/workspace/create/tdei.vue
Adds explicit local types, guards/getDatasetInfo null handling, removes existing Leaflet map before init, conditionally fits bounds only when valid, casts nullable IDs in creation payload with safe fallbacks.
Package Dependency
package.json
Adds new runtime dependency qrcode.

Sequence Diagram

sequenceDiagram
    actor User
    participant Picker as ProjectGroupPicker
    participant API as Backend API

    User->>Picker: Focus / Type search
    activate Picker
    Picker->>Picker: Debounce input
    Picker->>API: loadGroups(pageNo, searchText)
    activate API
    API-->>Picker: paginated groups
    deactivate API
    Picker->>Picker: Render items / highlight
    deactivate Picker

    User->>Picker: Arrow / Enter / Click item
    activate Picker
    Picker->>Picker: Update highlighted / select
    Picker->>Picker: Update model & close
    deactivate Picker

    User->>Picker: Scroll near bottom
    activate Picker
    Picker->>API: loadGroups(nextPage, searchText)
    activate API
    API-->>Picker: next page
    deactivate API
    Picker->>Picker: Append items
    deactivate Picker
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibbled keys and scrolled with cheer,

Pages fetched as results drew near,
Arrow hops, Enter's gentle thrum,
Types secured where bugs were numb,
A tiny QR joined our burrow — yum!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding a searchable project group picker component and making TypeScript/robustness fixes to TDEI service integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 47 minutes and 58 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread package.json
Comment thread services/tdei.ts Outdated
Comment thread services/tdei.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/tdei.ts (1)

17-27: ⚠️ Potential issue | 🟠 Major

Decode JWT payload as base64url before atob.

At line 26, atob(body) assumes standard base64. JWT payloads are base64url-encoded per RFC 7519 and can include - and _ characters. These will produce incorrect decoding with atob() and cause JSON parse failures for valid tokens.

🔧 Proposed fix
function getJwtBody(accessToken: string): any {
  const bodyStart = accessToken.indexOf('.');
  const bodyEnd = accessToken.indexOf('.', bodyStart + 1);

  if (bodyStart === -1 || bodyEnd === -1) {
    throw new Error('Error parsing JWT body');
  }

-  let body = accessToken.substring(bodyStart + 1, bodyEnd);
-  body = JSON.parse(atob(body))
+  const body = accessToken.substring(bodyStart + 1, bodyEnd);
+  const normalized = body.replace(/-/g, '+').replace(/_/g, '/');
+  const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4);
+  const parsed = JSON.parse(atob(padded));

-  return body;
+  return parsed;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tdei.ts` around lines 17 - 27, getJwtBody currently runs atob on the
JWT payload which is base64url-encoded; update getJwtBody to normalize the
extracted payload string (variable body) from base64url to standard base64 by
replacing '-' -> '+' and '_' -> '/' and adding '=' padding to reach a length
multiple of 4, then call atob on the normalized string and JSON.parse the
result; keep the existing JWT part extraction and existing error throw for
malformed tokens and ensure JSON.parse runs on the decoded payload.
🧹 Nitpick comments (1)
pages/workspace/create/tdei.vue (1)

232-235: Prefer explicit null guards over as string casts in create().

At Lines 232-233, casts can mask null runtime values. Since this payload is forwarded to import/create calls, a defensive guard here will prevent invalid requests.

🔧 Proposed refactor
 async function create() {
+  if (!tdeiRecordId.value || !projectGroupId.value) {
+    return
+  }
+
   const workspaceId = await importer.import({
     title: workspaceTitle.value,
     type: record.data_type,
-    tdeiRecordId: tdeiRecordId.value as string,
-    tdeiProjectGroupId: projectGroupId.value as string,
+    tdeiRecordId: tdeiRecordId.value,
+    tdeiProjectGroupId: projectGroupId.value,
     tdeiServiceId: record.service?.tdei_service_id || '',
     tdeiMetadata: JSON.stringify(record),
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/workspace/create/tdei.vue` around lines 232 - 235, In the create()
payload construction the fields tdeiRecordId: tdeiRecordId.value as string and
tdeiProjectGroupId: projectGroupId.value as string use unsafe casts; replace
these casts with explicit null/undefined guards inside the create() function
(e.g., check tdeiRecordId.value and projectGroupId.value before building the
payload), and either throw a descriptive error or return early if they are
missing so you never send an invalid request; update the code paths that call
the import/create API to rely on these validated values (references:
tdeiRecordId, projectGroupId, create()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/ProjectGroupPicker.vue`:
- Around line 61-70: The loadGroups function currently bails out immediately
when loading.value is true, discarding concurrent watcher-triggered calls; add a
small retry queue by introducing a pendingLoad flag (and optionally
pendingReset) so that if loadGroups(reset) is invoked while loading.value is
true it records pendingLoad = true (and pendingReset = pendingReset || reset)
then returns; at the end of the actual network flow inside loadGroups
(success/failure/finally) check pendingLoad and if set call
loadGroups(pendingReset) and clear pendingLoad/pendingReset. Update the watcher
(the one that currently calls loadGroups on search/page changes) to simply call
loadGroups() as before (it will now enqueue instead of drop), ensuring you
reference loadGroups, loading.value, pendingLoad, and pendingReset when making
the change.
- Around line 215-218: The current logic in ProjectGroupPicker.vue overwrites a
preexisting selection by setting model.value to projectGroups.value[0]?.id when
model.value is truthy but not found in the currently loaded page; change this so
we only assign the first project group when there is no existing selection (use
a null/undefined check or !model.value strictly for empty), i.e. remove the
branch that replaces a non-null model when it isn't in projectGroups.value and
only set model.value = projectGroups.value[0]?.id when model.value is
null/undefined; reference the projectGroups.value and model.value checks in the
component to implement this guard.
- Around line 3-10: ProjectGroupPicker is not declaring or binding a disabled
prop so the parent's :disabled ends up on the wrapper div instead of the actual
<input>; declare a prop named disabled (Boolean) on the component and bind it to
the input element (e.g., add :disabled="disabled" to the <input> that uses
v-model="searchText"), or alternatively set inheritAttrs: false and explicitly
forward attributes to the input; update the component's props block (or script
setup) and the input element (where onFocus/onKeydown are defined) to ensure the
disabled state is applied to the actual input.

In `@services/tdei.ts`:
- Around line 386-388: The assignments to this.#auth.subject,
this.#auth.displayName, and this.#auth.email currently use casts
(jwt.sub/name/email as unknown as string) without runtime checks; update the
code (where jwt is handled and before these assignments) to validate that
jwt.sub, jwt.name, and jwt.email are strings (and non-empty if required) and
only then assign them to this.#auth.subject/displayName/email, otherwise set a
safe fallback (e.g., empty string) or throw an explicit error; also ensure any
code that builds URLs in getMyProjectGroups defensively handles missing/invalid
this.#auth values so it won't produce fields like project-group-roles/undefined.

---

Outside diff comments:
In `@services/tdei.ts`:
- Around line 17-27: getJwtBody currently runs atob on the JWT payload which is
base64url-encoded; update getJwtBody to normalize the extracted payload string
(variable body) from base64url to standard base64 by replacing '-' -> '+' and
'_' -> '/' and adding '=' padding to reach a length multiple of 4, then call
atob on the normalized string and JSON.parse the result; keep the existing JWT
part extraction and existing error throw for malformed tokens and ensure
JSON.parse runs on the decoded payload.

---

Nitpick comments:
In `@pages/workspace/create/tdei.vue`:
- Around line 232-235: In the create() payload construction the fields
tdeiRecordId: tdeiRecordId.value as string and tdeiProjectGroupId:
projectGroupId.value as string use unsafe casts; replace these casts with
explicit null/undefined guards inside the create() function (e.g., check
tdeiRecordId.value and projectGroupId.value before building the payload), and
either throw a descriptive error or return early if they are missing so you
never send an invalid request; update the code paths that call the import/create
API to rely on these validated values (references: tdeiRecordId, projectGroupId,
create()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 005d350a-6a48-47a7-9fcf-3e20686d3301

📥 Commits

Reviewing files that changed from the base of the PR and between f37101b and 2083f1f.

📒 Files selected for processing (4)
  • components/ProjectGroupPicker.vue
  • package.json
  • pages/workspace/create/tdei.vue
  • services/tdei.ts

Comment thread components/ProjectGroupPicker.vue
Comment thread components/ProjectGroupPicker.vue
Comment thread components/ProjectGroupPicker.vue
Comment thread services/tdei.ts Outdated
Add explicit TDEI API and normalized types and update service clients to use them. services/tdei.ts: import and apply new types, add return types, safer JSON parsing and JWT field checks, and better error handling in getMyProjectGroups. types/tdei.ts: introduce API response shapes and normalized TdeiProjectGroup/TdeiService/TdeiDatasetSummary interfaces. components/ProjectGroupPicker.vue: add disabled prop, implement pendingReset logic to queue resets while loading, and simplify initial selection logic. pages/workspace/create/tdei.vue: replace manual property copy with Object.assign and guard against null dataset info.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pages/workspace/create/tdei.vue (1)

184-195: ⚠️ Potential issue | 🔴 Critical

Return early before dereferencing record.project_group.

if (!info) return only exits the loading.wrap callback, so the function still falls through to nextTick() and line 194 can throw when the API returns no dataset. Bail out from getDatasetInfo() itself here, or reset the form state and stop processing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/workspace/create/tdei.vue` around lines 184 - 195, The code currently
only returns from the loading.wrap callback when client.getDatasetInfo(id)
yields no info, but execution continues and dereferences record.project_group
(used to set projectGroupId.value) causing a crash; update the flow so that
after awaiting loading.wrap(...) you check whether the record was populated
(e.g., verify record.metadata or record.project_group or a boolean flag set
inside the callback) and if not, bail out from the enclosing function (or reset
form state) before calling nextTick() and before setting workspaceTitle.value,
projectGroupId.value, and tdeiRecordId.value; alternatively, move the early
return out of the loading.wrap and return from the outer async function right
when client.getDatasetInfo(id) returns no info (referencing loading.wrap,
tdeiClient, client.getDatasetInfo, record, nextTick, workspaceTitle.value,
projectGroupId.value, tdeiRecordId.value).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pages/workspace/create/tdei.vue`:
- Around line 184-188: The code currently shallow-merges API result into the
existing record with Object.assign(record, info), which can leave stale keys;
instead, when handling the getDatasetInfo response inside the loading.wrap
callback (the client from tdeiClient and the variables record and info), replace
the record wholesale or clear it before copying: either remove all existing keys
from record (e.g., delete each key) and then Object.assign(record, info), or set
record to the new info object/reactive copy so no old fields remain; update the
logic in the loading.wrap(...) callback where client.getDatasetInfo(id) is
handled to perform this replacement rather than a shallow merge.

---

Outside diff comments:
In `@pages/workspace/create/tdei.vue`:
- Around line 184-195: The code currently only returns from the loading.wrap
callback when client.getDatasetInfo(id) yields no info, but execution continues
and dereferences record.project_group (used to set projectGroupId.value) causing
a crash; update the flow so that after awaiting loading.wrap(...) you check
whether the record was populated (e.g., verify record.metadata or
record.project_group or a boolean flag set inside the callback) and if not, bail
out from the enclosing function (or reset form state) before calling nextTick()
and before setting workspaceTitle.value, projectGroupId.value, and
tdeiRecordId.value; alternatively, move the early return out of the loading.wrap
and return from the outer async function right when client.getDatasetInfo(id)
returns no info (referencing loading.wrap, tdeiClient, client.getDatasetInfo,
record, nextTick, workspaceTitle.value, projectGroupId.value,
tdeiRecordId.value).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2c52b9a1-a7bd-4112-b0e9-aad064f2e498

📥 Commits

Reviewing files that changed from the base of the PR and between 2083f1f and 4c3f55c.

📒 Files selected for processing (4)
  • components/ProjectGroupPicker.vue
  • pages/workspace/create/tdei.vue
  • services/tdei.ts
  • types/tdei.ts
✅ Files skipped from review due to trivial changes (1)
  • types/tdei.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/ProjectGroupPicker.vue
  • services/tdei.ts

Comment thread pages/workspace/create/tdei.vue
When loading dataset info in getDatasetInfo, clear any existing keys on the shared record object before Object.assign. This prevents stale/orphaned fields from a previously loaded dataset from persisting when switching datasets.
@sureshgaussian sureshgaussian requested a review from jeffmaki April 29, 2026 13:19
@jeffmaki jeffmaki merged commit 0d372da into master Apr 29, 2026
1 of 2 checks passed
@jeffmaki jeffmaki deleted the bugfix-3566-workspace-select-pg branch April 29, 2026 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants