[GettingStartedWidget] Fix Invite User widget crash on null deletedAt#1714
Conversation
…wire shapes
The Invite User widget's team search crashed with
"null is not an object (evaluating 't?.deletedAt.Valid')": renderOption
read `.Valid` on a `deletedAt` that the teams API returns as `null` for
live rows, and `null.Valid` throws.
`deletedAt` reaches the UI in three shapes depending on the endpoint and
serializer: `null`/`undefined` (canonical live row), an ISO timestamp
string (canonical deleted row), or the legacy Go `sql.NullTime`
`{ Valid, Time }` object still emitted by some provider endpoints.
Reading `.Valid` directly is unsafe for the first two.
Add `isSoftDeleted` and `getDeletedAtTime` helpers (src/utils/nullTime)
plus a shared `DeletedAt` union type that tolerate every shape, and route
all soft-delete checks through them: TeamSearchField, UserSearchField,
UserSearchFieldInput, WorkspaceCard, UsersTable, TeamTableConfiguration,
and Workspaces/helper. Several of these read `.Valid` directly and shared
the same latent crash.
Add unit tests covering every shape, including the null regression.
Signed-off-by: Lee Calcote <leecalcote@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request introduces utility functions (isSoftDeleted and getDeletedAtTime) to safely handle various formats of the deletedAt soft-delete timestamp, preventing runtime crashes when accessing properties on null or undefined values. These utilities are integrated across several components and tables. However, a critical bug was identified in TeamTableConfiguration.tsx where the wrong row data index (4 instead of 6) is passed to isSoftDeleted, which would incorrectly disable action buttons for all teams.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| searchable: false, | ||
| customBodyRender: (_: string, tableMeta: MUIDataTableMeta) => { | ||
| if (bulkSelect || tableMeta.rowData[4].Valid) { | ||
| if (bulkSelect || isSoftDeleted(tableMeta.rowData[4])) { |
There was a problem hiding this comment.
In TeamTableConfiguration, the deletedAt column is at index 6 of the columns array, while index 4 corresponds to createdAt. Using tableMeta.rowData[4] with isSoftDeleted will check the createdAt timestamp (which is a non-empty string), causing isSoftDeleted to always return true. This results in the edit and delete action buttons being permanently disabled for all teams.
Please update the index to 6 to correctly reference the deletedAt column.
| if (bulkSelect || isSoftDeleted(tableMeta.rowData[4])) { | |
| if (bulkSelect || isSoftDeleted(tableMeta.rowData[6])) { |
There was a problem hiding this comment.
Good catch — fixed in 64e4926. The index is now 6 (the deletedAt column) at this site, matching the already-correct setRowProps check, with a clarifying comment at both index sites. The old rowData[4].Valid was a latent wrong-index bug that was harmless only because .Valid on the createdAt string was undefined; isSoftDeleted correctly reads a timestamp string as deleted, which surfaced it.
Generated by Claude Code
There was a problem hiding this comment.
Pull request overview
This PR hardens the UI against deletedAt coming back as null / timestamp string / legacy { Valid, Time }, fixing a production crash caused by directly reading .Valid on a null value. It centralizes soft-delete detection into shared utilities and updates affected widgets/tables to use those helpers.
Changes:
- Added
DeletedAtunion type plusisSoftDeleted/getDeletedAtTimehelpers insrc/utils/nullTime.tsand exported them viasrc/utils/index.ts. - Updated multiple UI components to replace direct
.deletedAt.Validaccess withisSoftDeleted(...). - Added Jest coverage for all supported
deletedAtshapes, including thenullregression.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/utils/permissions.ts | Updates User.deletedAt typing to the new DeletedAt union. |
| src/utils/nullTime.ts | Introduces shared, shape-tolerant helpers for soft-delete handling. |
| src/utils/index.ts | Re-exports the new null-time utilities from the utils barrel. |
| src/custom/Workspaces/WorkspaceCard.tsx | Replaces .deletedAt.Valid checks with isSoftDeleted(...) to prevent null crashes. |
| src/custom/Workspaces/types.ts | Aligns workspace/team deletedAt types with the shared DeletedAt union. |
| src/custom/Workspaces/helper.ts | Switches deletion timestamp formatting to use getDeletedAtTime(...). |
| src/custom/UsersTable/UsersTable.tsx | Uses isSoftDeleted(...) when deciding whether to disable row actions. |
| src/custom/UserSearchField/UserSearchFieldInput.tsx | Prevents selecting deleted users using isSoftDeleted(...) instead of .Valid. |
| src/custom/UserSearchField/UserSearchField.tsx | Uses isSoftDeleted(...) for option rendering (“(deleted)”) safely across shapes. |
| src/custom/TeamTable/TeamTableConfiguration.tsx | Replaces multiple .Valid reads with isSoftDeleted(...) in table behaviors. |
| src/custom/DashboardWidgets/GettingStartedWidget/TeamSearchField.tsx | Fixes the reported Invite User widget crash by avoiding .Valid on null. |
| src/testing/nullTime.test.ts | Adds tests covering null/undefined, string timestamps, and legacy object shape. |
| export const parseDeletionTimestamp = (data: { deletedAt: DeletedAt }) => { | ||
| const time = getDeletedAtTime(data?.deletedAt); | ||
| return time != null ? getFormatDate(time as string) : DEFAULT_DATE; | ||
| }; |
There was a problem hiding this comment.
Fixed in 64e4926. Dropped the as string cast and widened getFormatDate to accept string | number | Date (it already calls new Date(date) internally, which handles all three), so getDeletedAtTime's return value flows through without a cast and TS stays honest about the shape.
Generated by Claude Code
Address review feedback on the soft-delete hardening: - TeamTableConfiguration action-column render read `rowData[4]` (createdAt) instead of `rowData[6]` (deletedAt). The old `rowData[4].Valid` was harmlessly undefined on the createdAt string, but `isSoftDeleted` correctly treats a non-empty timestamp string as deleted, which turned the pre-existing wrong-index bug into a real regression that permanently disabled every team's edit/delete actions. Use index 6 to match the deletedAt column (and the already-correct setRowProps check), with a clarifying comment at both index sites. - parseDeletionTimestamp no longer casts the deletion time with `as string`. getDeletedAtTime can return string | number | Date, so widen getFormatDate to accept all three (it already calls new Date(date)) and pass the value through without a cast. Signed-off-by: Lee Calcote <leecalcote@gmail.com>
Notes for Reviewers
The Invite User widget's team search crashed in production with:
Root cause.
TeamSearchField.renderOption(and several sibling components) read.Validdirectly offdeletedAt. A soft-delete timestamp reaches the UI in three different shapes depending on the endpoint and serializer:null/undefined— the canonicalmeshery/schemaswire shape for a live row (core.NullTimemarshals a non-deleted row to JSONnull). This is what the teams API returns, sonull.Validthrew.type: string, format: date-time).sql.NullTime{ Valid, Time }object — still emitted by some provider endpoints.Reading
.Validdirectly is unsafe for the first two shapes, and semantically wrong against the canonical contract (where.Validnever exists).Fix (at the root, in the shared library). Add
isSoftDeletedandgetDeletedAtTimehelpers plus a sharedDeletedAtunion type insrc/utils/nullTime.ts, and route every soft-delete check through them. This mirrors the multi-shape helper meshery-cloud already maintains (ui/utility/deletedAt.ts). Components updated:DashboardWidgets/GettingStartedWidget/TeamSearchField(the Invite User widget — the reported crash)UserSearchField,UserSearchFieldInputWorkspaces/WorkspaceCard,Workspaces/helper(parseDeletionTimestamp)TeamTable/TeamTableConfiguration,UsersTableSeveral of these (
WorkspaceCard,TeamTableConfiguration,UsersTable) read.Validwith no guard at all and shared the identical latent crash.Tests. New
src/__testing__/nullTime.test.tscovers every shape, including thenullregression. Full suite: 349 passing.npm run buildandeslintclean.Signed commits