Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion services/platform/app/components/layout/content-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { forwardRef, type HTMLAttributes } from 'react';

import { cn } from '@/lib/utils/cn';

const contentAreaVariants = cva('flex w-full flex-col', {
const contentAreaVariants = cva('flex min-w-0 w-full flex-col', {
variants: {
variant: {
page: 'px-4 py-6',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export function DataTableEmptyState({
}: DataTableEmptyStateProps) {
return (
<Center className="flex-[1_1_0] py-12">
<VStack align="center" className="max-w-[24rem] text-center">
<VStack align="center" className="max-w-[24rem] gap-2 text-center">
{Icon && (
<Icon
className="text-muted-foreground/60 mb-3 size-10"
className="text-muted-foreground/60 mb-3 size-6"
aria-hidden="true"
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ export function DataTable<TData, TValue = unknown>({
)}
<div
ref={scrollContainerRef}
className="border-border min-h-0 overflow-auto rounded-xl border"
className="border-border min-h-0 overflow-auto overscroll-contain rounded-xl border"
>
{tableContent}
{infiniteScrollContent}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ interface EntityRowActionsProps {
contentWidth?: string;
/** Alignment of dropdown */
align?: 'start' | 'center' | 'end';
/** Whether the entire menu trigger is disabled */
disabled?: boolean;
}

/**
Expand All @@ -68,6 +70,7 @@ export const EntityRowActions = React.memo(function EntityRowActions({
triggerClassName,
contentWidth = 'w-[10rem]',
align = 'end',
disabled = false,
}: EntityRowActionsProps) {
const { t: tCommon } = useT('common');
const [isOpen, setIsOpen] = useState(false);
Expand Down Expand Up @@ -115,13 +118,14 @@ export const EntityRowActions = React.memo(function EntityRowActions({
icon={MoreVertical}
aria-label={ariaLabel || tCommon('actions.openMenu')}
className={triggerClassName}
disabled={disabled}
/>
}
items={menuItems}
align={align}
contentClassName={contentWidth}
open={isOpen}
onOpenChange={setIsOpen}
open={disabled ? false : isOpen}
onOpenChange={disabled ? undefined : setIsOpen}
/>
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// @vitest-environment jsdom
import '@testing-library/jest-dom/vitest';
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const mockToast = vi.fn();
const mockUploadFiles = vi.fn().mockResolvedValue({ success: true });
const mockCancelUpload = vi.fn();
const mockClearTrackedFiles = vi.fn();
const mockRetryFile = vi.fn();
const mockRetryAllFailed = vi.fn();
const mockRemoveTrackedFile = vi.fn();

vi.mock('@/lib/i18n/client', () => ({
useT: (ns: string) => ({
t: (key: string, params?: Record<string, unknown>) => {
if (params) {
return Object.entries(params).reduce(
(acc, [k, v]) => acc.replace(`{${k}}`, String(v)),
`${ns}.${key}`,
);
}
return `${ns}.${key}`;
},
}),
}));

vi.mock('@/app/hooks/use-organization-id', () => ({
useOrganizationId: () => 'org-1',
}));

vi.mock('@/app/hooks/use-toast', () => ({
toast: (...args: unknown[]) => mockToast(...args),
}));

vi.mock('@/app/hooks/use-team-filter', () => ({
useTeamFilter: () => ({ selectedTeamId: null }),
}));

const mockTeams = [
{ id: 'team-1', name: 'Sales' },
{ id: 'team-2', name: 'Support' },
];

vi.mock('@/app/features/settings/teams/hooks/queries', () => ({
useTeams: () => ({ teams: mockTeams, isLoading: false }),
}));

interface MockTrackedFile {
id: string;
file: File;
status: string;
bytesLoaded: number;
bytesTotal: number;
error?: string;
}

let mockHookState: {
isUploading: boolean;
trackedFiles: MockTrackedFile[];
completedCount: number;
failedCount: number;
totalCount: number;
allCompleted: boolean;
hasFailures: boolean;
} = {
isUploading: false,
trackedFiles: [],
completedCount: 0,
failedCount: 0,
totalCount: 0,
allCompleted: false,
hasFailures: false,
};

vi.mock('../../hooks/mutations', () => ({
useDocumentUpload: () => ({
uploadFiles: mockUploadFiles,
retryFile: mockRetryFile,
retryAllFailed: mockRetryAllFailed,
isUploading: mockHookState.isUploading,
trackedFiles: mockHookState.trackedFiles,
removeTrackedFile: mockRemoveTrackedFile,
clearTrackedFiles: mockClearTrackedFiles,
cancelUpload: mockCancelUpload,
completedCount: mockHookState.completedCount,
failedCount: mockHookState.failedCount,
totalCount: mockHookState.totalCount,
allCompleted: mockHookState.allCompleted,
hasFailures: mockHookState.hasFailures,
}),
}));

import { DocumentUploadDialog } from '../document-upload-dialog';

afterEach(cleanup);

beforeEach(() => {
vi.clearAllMocks();
mockHookState = {
isUploading: false,
trackedFiles: [],
completedCount: 0,
failedCount: 0,
totalCount: 0,
allCompleted: false,
hasFailures: false,
};
});

describe('DocumentUploadDialog', () => {
const defaultProps = {
open: true,
onOpenChange: vi.fn(),
organizationId: 'org-1',
};

it('renders dialog with title and drop zone', () => {
render(<DocumentUploadDialog {...defaultProps} />);

expect(
screen.getByText('documents.upload.importDocuments'),
).toBeInTheDocument();
expect(
screen.getByText('documents.upload.dropZoneTitle'),
).toBeInTheDocument();
});

it('renders team selection area with org-wide default', () => {
render(<DocumentUploadDialog {...defaultProps} />);

expect(
screen.getByText('documents.upload.selectTeams'),
).toBeInTheDocument();
expect(screen.getByText('documents.teamTags.orgWide')).toBeInTheDocument();
});

it('renders drop zone description with file types', () => {
render(<DocumentUploadDialog {...defaultProps} />);

expect(
screen.getByText(/documents\.upload\.dropZoneDescription/),
).toBeInTheDocument();
});

it('shows cancel button when uploading', () => {
mockHookState = {
isUploading: true,
trackedFiles: [
{
id: 'file-1',
file: new File(['content'], 'test.pdf', {
type: 'application/pdf',
}),
status: 'uploading',
bytesLoaded: 500,
bytesTotal: 1000,
},
],
completedCount: 0,
failedCount: 0,
totalCount: 1,
allCompleted: false,
hasFailures: false,
};

render(<DocumentUploadDialog {...defaultProps} />);

expect(
screen.getByText('documents.upload.cancelUpload'),
).toBeInTheDocument();
});

it('shows success banner when all files completed', () => {
mockHookState = {
isUploading: false,
trackedFiles: [
{
id: 'file-1',
file: new File(['content'], 'test.pdf', {
type: 'application/pdf',
}),
status: 'completed',
bytesLoaded: 1000,
bytesTotal: 1000,
},
],
completedCount: 1,
failedCount: 0,
totalCount: 1,
allCompleted: true,
hasFailures: false,
};

render(<DocumentUploadDialog {...defaultProps} />);

expect(
screen.getByText(/documents\.upload\.documentsUploadedSuccessfully/),
).toBeInTheDocument();
});

it('shows retry button when files have failed', () => {
mockHookState = {
isUploading: false,
trackedFiles: [
{
id: 'file-1',
file: new File(['content'], 'test.pdf', {
type: 'application/pdf',
}),
status: 'failed',
bytesLoaded: 0,
bytesTotal: 1000,
error: 'Upload failed',
},
],
completedCount: 0,
failedCount: 1,
totalCount: 1,
allCompleted: false,
hasFailures: true,
};

render(<DocumentUploadDialog {...defaultProps} />);

expect(
screen.getByText('documents.upload.retryUpload'),
).toBeInTheDocument();
expect(screen.getByText('common.actions.cancel')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// @vitest-environment jsdom
import '@testing-library/jest-dom/vitest';
import { cleanup, render, screen, fireEvent } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { TeamMultiSelect } from '../team-multi-select';

afterEach(cleanup);

const mockTeams = [
{ id: 'team-1', name: 'Sales' },
{ id: 'team-2', name: 'Support' },
{ id: 'team-3', name: 'Operations' },
];

const defaultProps = {
teams: mockTeams,
selectedTeamIds: [] as string[],
onSelectionChange: vi.fn(),
orgWideLabel: 'Organization-wide',
};

describe('TeamMultiSelect', () => {
it('renders org-wide chip when no teams selected', () => {
render(<TeamMultiSelect {...defaultProps} />);

expect(screen.getByText('Organization-wide')).toBeInTheDocument();
});

it('shows selected teams as chips', () => {
render(
<TeamMultiSelect
{...defaultProps}
selectedTeamIds={['team-1', 'team-2']}
/>,
);

expect(screen.getByText('Sales')).toBeInTheDocument();
expect(screen.getByText('Support')).toBeInTheDocument();
expect(screen.queryByText('Organization-wide')).not.toBeInTheDocument();
});

it('opens dropdown on click', () => {
render(<TeamMultiSelect {...defaultProps} />);

const trigger = screen.getByRole('button', { expanded: false });
fireEvent.click(trigger);

expect(screen.getByRole('listbox')).toBeInTheDocument();
expect(screen.getByText('Operations')).toBeInTheDocument();
});

it('calls onSelectionChange when toggling a team', () => {
const onSelectionChange = vi.fn();
render(
<TeamMultiSelect
{...defaultProps}
selectedTeamIds={['team-1']}
onSelectionChange={onSelectionChange}
/>,
);

// Open dropdown
fireEvent.click(screen.getByRole('button', { expanded: false }));

// Toggle Support on
fireEvent.click(screen.getByRole('option', { name: /support/i }));
expect(onSelectionChange).toHaveBeenCalledWith(['team-1', 'team-2']);
});

it('calls onSelectionChange when removing a team via chip', () => {
const onSelectionChange = vi.fn();
render(
<TeamMultiSelect
{...defaultProps}
selectedTeamIds={['team-1', 'team-2']}
onSelectionChange={onSelectionChange}
/>,
);

const removeButton = screen.getByRole('button', { name: /remove sales/i });
fireEvent.click(removeButton);
expect(onSelectionChange).toHaveBeenCalledWith(['team-2']);
});

it('disables trigger when disabled prop is true', () => {
render(<TeamMultiSelect {...defaultProps} disabled />);

const trigger = screen.getByRole('button');
expect(trigger).toBeDisabled();
});
});
Loading
Loading