Skip to content

fix: miscellaneous UI improvements across platform#798

Merged
Israeltheminer merged 6 commits into
mainfrom
fix/misc-ui-improvements
Mar 15, 2026
Merged

fix: miscellaneous UI improvements across platform#798
Israeltheminer merged 6 commits into
mainfrom
fix/misc-ui-improvements

Conversation

@Israeltheminer
Copy link
Copy Markdown
Collaborator

@Israeltheminer Israeltheminer commented Mar 15, 2026

Summary

  • Reduced empty state icon size (size-10 → size-6) and added consistent gap
  • Added disabled prop to EntityRowActions to prevent interaction after deletion
  • Disabled team row actions after team deletion to prevent stale operations
  • Changed documents empty state icon from ClipboardList to FileText
  • Fixed website view dialog: chunk content now scrollable (max-h-48), pages use grid layout
  • Replaced TableDateCell with CopyableTimestamp for website last scanned date
  • Enabled allowRemovingAllTeams in auth config for org-wide document support

Test plan

  • Verify empty state icons render at correct size across all tables
  • Delete a team and verify row actions become disabled
  • Check website view dialog pages display in grid layout
  • Verify chunk content sections are scrollable for long text
  • Confirm last scanned timestamp is copyable in website table and dialog

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Multi-team document uploads with per-file progress tracking and retry functionality
    • Team selection dropdown for assigning uploads to specific teams
    • Copyable timestamp display in website views
  • Improvements

    • Redesigned document upload dialog with enhanced progress tracking and status messages
    • Updated messaging terminology from "import" to "upload" throughout the platform
    • Better disabled state handling for action controls
    • Improved data table spacing and layout performance
    • Support for removing all teams in multi-tenant setups

…i-team assignment

Redesign the document upload dialog based on new design specifications:

- Replace FormDialog with base Dialog for custom layout
- Add per-file upload status tracking (pending/uploading/completed/failed)
- Create TeamMultiSelect component with checkbox dropdown and chip display
- Show "Organization-wide" chip as default when no teams selected
- Create UploadFileRow component with color-coded type badges and progress bars
- Support multi-team document assignment (teamIds array)
- Add success banner, progress summary, cancel/retry functionality
- Auto-upload on file selection instead of manual submit
- Fix layout overflow: add min-w-0 to ContentArea, overscroll-contain to DataTable
- Add tests for TeamMultiSelect, UploadFileRow, and DocumentUploadDialog
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

This PR introduces a multi-file document upload system with team selection. Changes include: refactoring DocumentUploadDialog to support multi-file uploads with per-file progress tracking using a new useDocumentUpload hook, adding TeamMultiSelect and UploadFileRow components for team assignment and file status display respectively, extending the uploadFiles API to accept multiple teamIds instead of a single teamId, updating auth configuration to allow removing all teams, implementing disabled state in EntityRowActions, improving team deletion handling, and adding comprehensive test coverage for the new upload dialog and components. Additional UI refinements include spacing adjustments, icon replacements, timestamp component updates, and translation changes reflecting "upload" terminology.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'fix: miscellaneous UI improvements across platform' uses the vague term 'miscellaneous,' which lacks specificity about the actual changes made. Consider a more specific title that highlights the primary change, such as 'fix: improve UI consistency with icon sizing, disabled states, and component updates' or focus on the main feature if applicable.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/misc-ui-improvements
📝 Coding Plan
  • Generate coding plan for human review comments

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

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: 7

Caution

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

⚠️ Outside diff range comments (1)
services/platform/lib/auth-client.ts (1)

51-58: ⚠️ Potential issue | 🔴 Critical

Remove allowRemovingAllTeams from client-side auth configuration.

allowRemovingAllTeams is a server-side only option for the organization() plugin and must not be configured in the client-side organizationClient() plugin. The client simply calls the normal remove-team API; the server enforces whether removing the last team is allowed. Delete lines 54 (the allowRemovingAllTeams: true line) from this client config.

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

In `@services/platform/lib/auth-client.ts` around lines 51 - 58, The client-side
organization/teams configuration in auth-client.ts incorrectly sets the
server-side-only option allowRemovingAllTeams; remove the allowRemovingAllTeams:
true property from the teams config object (the block passed to
organizationClient()/organization() in auth-client.ts) so the client no longer
sends this server-only flag — leave teams.enabled and teams.defaultTeam as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@services/platform/app/features/documents/components/__tests__/upload-file-row.test.tsx`:
- Around line 65-83: The test is matching two "failed" occurrences (filename
suffix and error) — update the assertions to be unambiguous: keep the explicit
error assertion expect(screen.getByText('Upload failed')).toBeInTheDocument()
and replace the general expect(screen.getByText(/failed/i)) with a scoped check
that the status text appears in the file row for UploadFileRow (use the file
name from baseProps to locate the row via screen.getByText(baseProps.file.name)
and then within(...) assert the status contains "Failed"), then keep the retry
button click and onRetry expectation unchanged.

In
`@services/platform/app/features/documents/components/document-upload-dialog.tsx`:
- Around line 52-54: The component currently initializes selectedTeamIds once
using selectedTeamId and never refreshes it when the dialog is reopened; update
logic so selectedTeamIds is reset from selectedTeamId whenever the dialog
transitions open (use the open prop), e.g. add a useEffect that watches open and
selectedTeamId and calls setSelectedTeamIds(selectedTeamId ? [selectedTeamId] :
[] ) when open becomes true; apply the same pattern for the other selection
state(s) referenced around lines 88-98 to ensure defaults are resynced on
reopen.

In `@services/platform/app/features/documents/components/team-multi-select.tsx`:
- Around line 75-110: The outer trigger currently renders as a <button> that
contains chip remove <button>s (see team-multi-select component using isOpen,
setIsOpen, disabled, teams, selectedTeams, orgWideLabel and removeTeam), which
creates nested interactive controls; change the outer trigger to a non-button
container (e.g., a div with role="button" and the same
onClick/aria-expanded/aria-haspopup behavior) or render the trigger and the
chips as separate sibling elements so chip remove buttons (removeTeam) are not
children of the trigger; ensure the disabled state is enforced by preventing the
outer click (check disabled || teams.length === 0) and keep chip buttons
independently operable with their aria-labels (e.g., `Remove ${team.name}`) and
styling intact.

In `@services/platform/app/features/documents/hooks/mutations.ts`:
- Around line 146-152: The cancel/remove logic only updates React state while
uploadFiles iterates a snapshot and abortControllerRef is nulled too early,
allowing hidden files to keep uploading; fix uploadFiles by creating a local
AbortController (e.g., const batchController = new AbortController()) at the
start of the batch, assign it to abortControllerRef.current but use the local
batchController.signal for all checks and pass that signal into
uploadSingleFile, ensure uploadFiles checks batchController.signal.aborted
before starting each file and breaks if aborted, and only clear
abortControllerRef.current (set to null) after the outer loop completes; also
ensure removeTrackedFile/clearTrackedFiles still update trackedFiles state but
do not attempt to control the batch—cancellation should call
abortControllerRef.current?.abort() so the batchController stops the actual
uploads.

In `@services/platform/app/features/websites/components/website-view-dialog.tsx`:
- Around line 354-357: The ternary rendering for website.lastScannedAt can be
simplified: replace the conditional that returns either <CopyableTimestamp
date={website.lastScannedAt} preset="long" /> or
<Text>{t('viewDialog.notScannedYet')}</Text> by always rendering
CopyableTimestamp and passing the fallback string via its emptyText prop (e.g.,
emptyText={t('viewDialog.notScannedYet')}), keeping preset="long" and removing
the conditional logic around website.lastScannedAt.

In `@services/platform/app/features/websites/hooks/use-websites-table-config.tsx`:
- Around line 95-100: The cell renderer in use-websites-table-config.tsx
currently checks row.original.lastScannedAt with a truthy guard which treats 0
as missing; update the condition in the cell function that renders
CopyableTimestamp to use a nullish check (row.original.lastScannedAt != null) so
valid epoch timestamps (including 0) render correctly; modify the conditional
around CopyableTimestamp accordingly.

---

Outside diff comments:
In `@services/platform/lib/auth-client.ts`:
- Around line 51-58: The client-side organization/teams configuration in
auth-client.ts incorrectly sets the server-side-only option
allowRemovingAllTeams; remove the allowRemovingAllTeams: true property from the
teams config object (the block passed to organizationClient()/organization() in
auth-client.ts) so the client no longer sends this server-only flag — leave
teams.enabled and teams.defaultTeam as-is.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 4e89b83c-701a-47c3-8942-02d09bbc4798

📥 Commits

Reviewing files that changed from the base of the PR and between 5fd1eda and 3c8da24.

📒 Files selected for processing (18)
  • services/platform/app/components/layout/content-area.tsx
  • services/platform/app/components/ui/data-table/data-table-empty-state.tsx
  • services/platform/app/components/ui/data-table/data-table.tsx
  • services/platform/app/components/ui/entity/entity-row-actions.tsx
  • services/platform/app/features/documents/components/__tests__/document-upload-dialog.test.tsx
  • services/platform/app/features/documents/components/__tests__/team-multi-select.test.tsx
  • services/platform/app/features/documents/components/__tests__/upload-file-row.test.tsx
  • services/platform/app/features/documents/components/document-upload-dialog.tsx
  • services/platform/app/features/documents/components/documents-table.tsx
  • services/platform/app/features/documents/components/team-multi-select.tsx
  • services/platform/app/features/documents/components/upload-file-row.tsx
  • services/platform/app/features/documents/hooks/mutations.ts
  • services/platform/app/features/settings/teams/components/team-row-actions.tsx
  • services/platform/app/features/websites/components/website-view-dialog.tsx
  • services/platform/app/features/websites/hooks/use-websites-table-config.tsx
  • services/platform/convex/auth.ts
  • services/platform/lib/auth-client.ts
  • services/platform/messages/en.json

Comment on lines +65 to +83
it('shows error state and retry button for failed files', () => {
const onRetry = vi.fn();
render(
<UploadFileRow
{...baseProps}
status="failed"
error="Upload failed"
onRetry={onRetry}
/>,
);

expect(screen.getByText(/failed/i)).toBeInTheDocument();
expect(screen.getByText('Upload failed')).toBeInTheDocument();

const retryButton = screen.getByRole('button', {
name: /retry/i,
});
fireEvent.click(retryButton);
expect(onRetry).toHaveBeenCalledOnce();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use a non-ambiguous assertion for the failed state.

Line 76 now matches both the filename suffix (— Failed) and the error message (Upload failed), which is why Test UI is failing with “Found multiple elements with the text: /failed/i”. Assert the specific error text or scope the status check to the filename row instead.

Proposed test fix
-    expect(screen.getByText(/failed/i)).toBeInTheDocument();
-    expect(screen.getByText('Upload failed')).toBeInTheDocument();
+    expect(screen.getByText(/test-document\.pdf/i)).toHaveTextContent(
+      /— Failed/,
+    );
+    expect(screen.getByText('Upload failed')).toBeInTheDocument();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('shows error state and retry button for failed files', () => {
const onRetry = vi.fn();
render(
<UploadFileRow
{...baseProps}
status="failed"
error="Upload failed"
onRetry={onRetry}
/>,
);
expect(screen.getByText(/failed/i)).toBeInTheDocument();
expect(screen.getByText('Upload failed')).toBeInTheDocument();
const retryButton = screen.getByRole('button', {
name: /retry/i,
});
fireEvent.click(retryButton);
expect(onRetry).toHaveBeenCalledOnce();
it('shows error state and retry button for failed files', () => {
const onRetry = vi.fn();
render(
<UploadFileRow
{...baseProps}
status="failed"
error="Upload failed"
onRetry={onRetry}
/>,
);
expect(screen.getByText(/test-document\.pdf/i)).toHaveTextContent(
/ Failed/,
);
expect(screen.getByText('Upload failed')).toBeInTheDocument();
const retryButton = screen.getByRole('button', {
name: /retry/i,
});
fireEvent.click(retryButton);
expect(onRetry).toHaveBeenCalledOnce();
🧰 Tools
🪛 GitHub Check: Test UI

[failure] 76-76: app/features/documents/components/tests/upload-file-row.test.tsx > UploadFileRow > shows error state and retry button for failed files
TestingLibraryElementError: Found multiple elements with the text: /failed/i

Here are the matching elements:

Ignored nodes: comments, script, style
<span
class="min-w-0 flex-1 truncate text-xs font-medium text-red-600"

test-document.pdf
— Failed

Ignored nodes: comments, script, style
<span
class="text-[11px] leading-snug text-red-800"

Upload failed

(If this is intentional, then use the *AllBy* variant of the query (like queryAllByText, getAllByText, or findAllByText)).

Ignored nodes: comments, script, style

PDF test-document.pdf — Failed 1.1 MB documents.upload.retry
Upload failed
❯ Object.getElementError ../../node_modules/@testing-library/dom/dist/config.js:37:19 ❯ getElementError ../../node_modules/@testing-library/dom/dist/query-helpers.js:20:35 ❯ getMultipleElementsFoundError ../../node_modules/@testing-library/dom/dist/query-helpers.js:23:10 ❯ ../../node_modules/@testing-library/dom/dist/query-helpers.js:55:13 ❯ ../../node_modules/@testing-library/dom/dist/query-helpers.js:95:19 ❯ app/features/documents/components/__tests__/upload-file-row.test.tsx:76:19
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/documents/components/__tests__/upload-file-row.test.tsx`
around lines 65 - 83, The test is matching two "failed" occurrences (filename
suffix and error) — update the assertions to be unambiguous: keep the explicit
error assertion expect(screen.getByText('Upload failed')).toBeInTheDocument()
and replace the general expect(screen.getByText(/failed/i)) with a scoped check
that the status text appears in the file row for UploadFileRow (use the file
name from baseProps to locate the row via screen.getByText(baseProps.file.name)
and then within(...) assert the status contains "Failed"), then keep the retry
button click and onRetry expectation unchanged.

Comment on lines +52 to +54
const [selectedTeamIds, setSelectedTeamIds] = useState<string[]>(() =>
selectedTeamId ? [selectedTeamId] : [],
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resync the default team selection when reopening the dialog.

This state only snapshots selectedTeamId on mount and when the dialog closes itself. If the global team filter changes while the dialog stays mounted but closed, reopening it reuses the old selection and uploads to the wrong team set. Sync selectedTeamIds from selectedTeamId when open flips to true, or derive that default in an effect tied to the open transition.

Also applies to: 88-98

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

In
`@services/platform/app/features/documents/components/document-upload-dialog.tsx`
around lines 52 - 54, The component currently initializes selectedTeamIds once
using selectedTeamId and never refreshes it when the dialog is reopened; update
logic so selectedTeamIds is reset from selectedTeamId whenever the dialog
transitions open (use the open prop), e.g. add a useEffect that watches open and
selectedTeamId and calls setSelectedTeamIds(selectedTeamId ? [selectedTeamId] :
[] ) when open becomes true; apply the same pattern for the other selection
state(s) referenced around lines 88-98 to ensure defaults are resynced on
reopen.

Comment on lines +75 to +110
<button
type="button"
onClick={() =>
!disabled && teams.length > 0 && setIsOpen((prev) => !prev)
}
disabled={disabled || teams.length === 0}
aria-expanded={isOpen}
aria-haspopup="listbox"
className={cn(
'flex w-full items-center gap-1.5 rounded-lg border bg-background px-3 py-2 text-sm shadow-xs transition-colors',
isOpen
? 'border-primary ring-2 ring-primary/20'
: 'border-border hover:border-border/80',
disabled && 'cursor-not-allowed opacity-50',
)}
>
<div className="flex flex-1 flex-wrap items-center gap-1.5">
{selectedTeams.length === 0 ? (
<span className="bg-muted inline-flex items-center rounded px-2 py-0.5 text-xs font-medium">
{orgWideLabel}
</span>
) : (
selectedTeams.map((team) => (
<span
key={team.id}
className="bg-muted inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs font-medium"
>
{team.name}
<button
type="button"
onClick={(e) => removeTeam(team.id, e)}
className="text-muted-foreground hover:text-foreground -mr-0.5 rounded-sm"
aria-label={`Remove ${team.name}`}
>
<X className="size-3" />
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't nest chip remove buttons inside the trigger.

Line 103 renders a <button> inside the outer trigger <button>. That's invalid interactive nesting, and it makes the disabled contract unreliable because the chip controls remain separate interactive targets. Split the trigger and chip actions into separate siblings, or render the outer shell as a non-button container and disable the chip buttons explicitly.

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

In `@services/platform/app/features/documents/components/team-multi-select.tsx`
around lines 75 - 110, The outer trigger currently renders as a <button> that
contains chip remove <button>s (see team-multi-select component using isOpen,
setIsOpen, disabled, teams, selectedTeams, orgWideLabel and removeTeam), which
creates nested interactive controls; change the outer trigger to a non-button
container (e.g., a div with role="button" and the same
onClick/aria-expanded/aria-haspopup behavior) or render the trigger and the
chips as separate sibling elements so chip remove buttons (removeTeam) are not
children of the trigger; ensure the disabled state is enforced by preventing the
outer click (check disabled || teams.length === 0) and keep chip buttons
independently operable with their aria-labels (e.g., `Remove ${team.name}`) and
styling intact.

Comment on lines +146 to +152
const removeTrackedFile = useCallback((fileId: string) => {
setTrackedFiles((prev) => prev.filter((f) => f.id !== fileId));
}, []);

const totalBytes = files.reduce((sum, f) => sum + f.size, 0);
perFileLoadedRef.current = new Array(files.length).fill(0);
setUploadProgress({
completedFiles: 0,
totalFiles: files.length,
bytesLoaded: 0,
bytesTotal: totalBytes,
});
const clearTrackedFiles = useCallback(() => {
setTrackedFiles([]);
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Cancel/removal only hides queued files; the batch can keep running.

uploadFiles iterates the newTracked snapshot, while removeTrackedFile/clearTrackedFiles only mutate React state. On top of that, Line 399 clears abortControllerRef.current immediately, so the next if (abortControllerRef.current?.signal.aborted) check sees null and proceeds with the remaining files. After a user clicks Cancel—or removes pending rows—the hidden files can still upload in the background. Keep a stable controller local to the batch, pass that signal through uploadSingleFile, and only clear the ref once the outer loop has exited.

Also applies to: 298-320, 396-401

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

In `@services/platform/app/features/documents/hooks/mutations.ts` around lines 146
- 152, The cancel/remove logic only updates React state while uploadFiles
iterates a snapshot and abortControllerRef is nulled too early, allowing hidden
files to keep uploading; fix uploadFiles by creating a local AbortController
(e.g., const batchController = new AbortController()) at the start of the batch,
assign it to abortControllerRef.current but use the local batchController.signal
for all checks and pass that signal into uploadSingleFile, ensure uploadFiles
checks batchController.signal.aborted before starting each file and breaks if
aborted, and only clear abortControllerRef.current (set to null) after the outer
loop completes; also ensure removeTrackedFile/clearTrackedFiles still update
trackedFiles state but do not attempt to control the batch—cancellation should
call abortControllerRef.current?.abort() so the batchController stops the actual
uploads.

Comment on lines 167 to +223
const contentHash = await calculateFileHash(file);

// Step 2: Get upload URL from Convex
const uploadUrl = await generateUploadUrl({});

// Step 3: Upload file with progress tracking
const { storageId } = await uploadWithProgress(
uploadUrl,
file,
resolvedType,
abortControllerRef.current?.signal,
(loaded) => {
perFileLoadedRef.current[fileIndex] = loaded;
const bytesLoaded = perFileLoadedRef.current.reduce(
(sum, v) => sum + v,
0,
);
setUploadProgress((prev) =>
prev ? { ...prev, bytesLoaded } : prev,
);
(loaded, total) => {
updateFileStatus(fileId, {
bytesLoaded: loaded,
bytesTotal: total,
});
},
);

// Step 4: Create document record in database
const result = await createDocumentFromUpload({
organizationId: options.organizationId,
fileId: toId<'_storage'>(storageId),
fileName: file.name,
contentType: resolvedType,
contentHash,
metadata: {
size: file.size,
sourceProvider: 'upload',
sourceMode: 'manual',
},
teamId: uploadOptions?.teamId,
folderId: uploadOptions?.folderId
? toId<'folders'>(uploadOptions.folderId)
: undefined,
fileSize: file.size,
});
// Create document records — one per team, or one org-wide
const teamIds = uploadOptions?.teamIds;
if (teamIds && teamIds.length > 0) {
for (const teamId of teamIds) {
await createDocumentFromUpload({
organizationId: options.organizationId,
fileId: toId<'_storage'>(storageId),
fileName: file.name,
contentType: resolvedType,
contentHash,
metadata: {
size: file.size,
sourceProvider: 'upload',
sourceMode: 'manual',
},
teamId,
folderId: uploadOptions?.folderId
? toId<'folders'>(uploadOptions.folderId)
: undefined,
fileSize: file.size,
});
}
} else {
await createDocumentFromUpload({
organizationId: options.organizationId,
fileId: toId<'_storage'>(storageId),
fileName: file.name,
contentType: resolvedType,
contentHash,
metadata: {
size: file.size,
sourceProvider: 'upload',
sourceMode: 'manual',
},
teamId: undefined,
folderId: uploadOptions?.folderId
? toId<'folders'>(uploadOptions.folderId)
: undefined,
fileSize: file.size,
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Multi-team retries are not idempotent.

After the blob is stored, Lines 185-204 fan out createDocumentFromUpload one team at a time from the client. If one team creation succeeds and a later one fails, the file is marked failed; retrying then uploads a second blob and replays all team creates. That can duplicate documents for teams that already succeeded and orphan the first storage object. The team fan-out needs to happen behind a single server-side mutation, or the mutation needs an idempotency key covering the uploaded content and target team/folder.

Comment on lines +354 to +357
value: website.lastScannedAt ? (
<CopyableTimestamp date={website.lastScannedAt} preset="long" />
) : (
<Text>{t('viewDialog.notScannedYet')}</Text>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Optional: simplify lastScannedAt branch with emptyText.

You can remove the conditional and keep one rendering path by delegating fallback text to CopyableTimestamp.

♻️ Proposed refactor
-        value: website.lastScannedAt ? (
-          <CopyableTimestamp date={website.lastScannedAt} preset="long" />
-        ) : (
-          <Text>{t('viewDialog.notScannedYet')}</Text>
-        ),
+        value: (
+          <CopyableTimestamp
+            date={website.lastScannedAt ?? null}
+            preset="long"
+            emptyText={t('viewDialog.notScannedYet')}
+          />
+        ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
value: website.lastScannedAt ? (
<CopyableTimestamp date={website.lastScannedAt} preset="long" />
) : (
<Text>{t('viewDialog.notScannedYet')}</Text>
value: (
<CopyableTimestamp
date={website.lastScannedAt ?? null}
preset="long"
emptyText={t('viewDialog.notScannedYet')}
/>
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/websites/components/website-view-dialog.tsx`
around lines 354 - 357, The ternary rendering for website.lastScannedAt can be
simplified: replace the conditional that returns either <CopyableTimestamp
date={website.lastScannedAt} preset="long" /> or
<Text>{t('viewDialog.notScannedYet')}</Text> by always rendering
CopyableTimestamp and passing the fallback string via its emptyText prop (e.g.,
emptyText={t('viewDialog.notScannedYet')}), keeping preset="long" and removing
the conditional logic around website.lastScannedAt.

Comment on lines 95 to 100
cell: ({ row }) =>
row.original.lastScannedAt ? (
<TableDateCell
date={row.original.lastScannedAt}
preset="short"
<CopyableTimestamp
date={row.original.lastScannedAt}
preset="long"
alignRight
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use a nullish check for lastScannedAt instead of a truthy check.

A truthy guard will treat 0 as missing and show the loading spinner. Use != null so valid epoch timestamps still render.

Suggested fix
-      cell: ({ row }) =>
-        row.original.lastScannedAt ? (
+      cell: ({ row }) =>
+        row.original.lastScannedAt != null ? (
           <CopyableTimestamp
           date={row.original.lastScannedAt}
           preset="long"
             alignRight
           />
         ) : (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cell: ({ row }) =>
row.original.lastScannedAt ? (
<TableDateCell
date={row.original.lastScannedAt}
preset="short"
<CopyableTimestamp
date={row.original.lastScannedAt}
preset="long"
alignRight
cell: ({ row }) =>
row.original.lastScannedAt != null ? (
<CopyableTimestamp
date={row.original.lastScannedAt}
preset="long"
alignRight
/>
) : (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/websites/hooks/use-websites-table-config.tsx`
around lines 95 - 100, The cell renderer in use-websites-table-config.tsx
currently checks row.original.lastScannedAt with a truthy guard which treats 0
as missing; update the condition in the cell function that renders
CopyableTimestamp to use a nullish check (row.original.lastScannedAt != null) so
valid epoch timestamps (including 0) render correctly; modify the conditional
around CopyableTimestamp accordingly.

@Israeltheminer Israeltheminer force-pushed the fix/misc-ui-improvements branch from 3c8da24 to 436bbcd Compare March 15, 2026 22:26
- Reduce empty state icon size from size-10 to size-6 and add gap-2
- Add disabled prop to EntityRowActions to prevent interaction after deletion
- Disable team row actions after team deletion to prevent stale operations
- Change documents empty state icon from ClipboardList to FileText
- Fix website view dialog: add chunk content scroll, grid layout for pages
- Use CopyableTimestamp for website last scanned date
- Allow removing all teams in auth config (allowRemovingAllTeams)
@Israeltheminer Israeltheminer force-pushed the fix/misc-ui-improvements branch from 436bbcd to 775a2c9 Compare March 15, 2026 22:28
@Israeltheminer Israeltheminer merged commit cc0d6b1 into main Mar 15, 2026
17 checks passed
@Israeltheminer Israeltheminer deleted the fix/misc-ui-improvements branch March 15, 2026 22:38
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.

1 participant