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
15 changes: 0 additions & 15 deletions design/comments.md

This file was deleted.

Binary file removed design/images/comments/knowledge-import-button.png
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface CopyableTimestampProps {
date: number | Date | string | null | undefined;
/** Format preset for display */
preset?: DatePreset;
/** Custom dayjs format string (overrides preset for display) */
customFormat?: string;
/** Additional className */
className?: string;
/** Text to show when date is null/undefined */
Expand All @@ -35,6 +37,7 @@ interface CopyableTimestampProps {
export const CopyableTimestamp = React.memo(function CopyableTimestamp({
date,
preset = 'short',
customFormat,
className,
emptyText = '—',
alignRight = false,
Expand Down Expand Up @@ -63,9 +66,12 @@ export const CopyableTimestamp = React.memo(function CopyableTimestamp({

const timestampMs = String(dateObj.valueOf());
const showTimezone = preset === 'long' || preset === 'time';
const formatted = showTimezone
? `${formatDate(dateObj, preset)} ${timezoneShort}`
const baseFormatted = customFormat
? formatDate(dateObj, preset, { customFormat })
: formatDate(dateObj, preset);
const formatted = showTimezone
? `${baseFormatted} ${timezoneShort}`
: baseFormatted;
const titleText = `${formatDate(dateObj, 'long')} (${timezone})`;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function createFilter(overrides?: Partial<FilterConfig>): FilterConfig {
async function openFilterPanel(
user: ReturnType<typeof import('@testing-library/user-event').default.setup>,
) {
const filterButton = screen.getByRole('button', { name: /filters/i });
const filterButton = screen.getByRole('button', { name: /filter/i });
await user.click(filterButton);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export function DataTableFilters({
}
>
<div className="border-border flex items-center justify-between p-3">
<Text as="span" variant="label" className="text-base">
<Text as="span" variant="label" className="text-sm">
{t('labels.filters')}
</Text>
{totalActiveFilters > 0 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const WithHeaderActions: Story = {
<Field label="File name">project-proposal.pdf</Field>
<Field label="Size">2.4 MB</Field>
<Field label="Created">January 10, 2024</Field>
<Field label="Modified">January 20, 2024</Field>
<Field label="Updated">January 20, 2024</Field>
</FieldGroup>
</ViewDialog>
</>
Expand Down
10 changes: 5 additions & 5 deletions services/platform/app/components/ui/filters/filter-button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Filter } from 'lucide-react';
import { ListFilter } from 'lucide-react';
import { Loader2Icon } from 'lucide-react';
import { ComponentProps } from 'react';

Expand All @@ -22,10 +22,9 @@ export function FilterButton({
return (
<Button
variant="secondary"
size="icon"
aria-label={t('labels.filters')}
aria-label={t('labels.filter')}
className={cn(
'hover:bg-muted relative p-2.5',
'hover:bg-muted relative gap-2',
hasActiveFilters && 'border-primary',
isLoading && 'opacity-75',
className,
Expand All @@ -35,8 +34,9 @@ export function FilterButton({
{isLoading ? (
<Loader2Icon className="text-muted-foreground size-4 animate-spin" />
) : (
<Filter className="text-muted-foreground size-4" />
<ListFilter className="text-muted-foreground size-4" />
)}
{t('labels.filter')}
{hasActiveFilters && !isLoading && (
<div className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-blue-500" />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ vi.mock('@/lib/i18n/client', () => ({
useT: () => ({
t: (key: string, options?: Record<string, string | number>) => {
const translations: Record<string, string> = {
'labels.nSelected': '{{count}} selected',
'labels.nSelected': '{count} selected',
};
let value = translations[key] ?? key;
if (options) {
for (const [k, v] of Object.entries(options)) {
value = value.replace(`{{${k}}}`, String(v));
value = value.replace(`{${k}}`, String(v));
}
}
return value;
Expand All @@ -30,14 +30,13 @@ describe('FilterSection', () => {
};

describe('rendering', () => {
it('renders the title in uppercase', () => {
it('renders the title', () => {
render(
<FilterSection {...defaultProps}>
<span>Content</span>
</FilterSection>,
);
expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByText('Status')).toHaveClass('uppercase');
});

it('does not render children when collapsed', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ export function FilterSection({
<Text
as="span"
variant="label-sm"
className="text-muted-foreground/80 flex-1 text-left uppercase"
className="text-muted-foreground/80 flex-1 text-left"
>
{title}
</Text>
{selectedCount > 0 && (
<span className="rounded-xl bg-blue-100/20 px-1.5 py-0.5 text-[10px] leading-3 font-medium text-blue-600">
<span className="bg-muted text-muted-foreground rounded-xl px-2 py-0.5 text-[10px] leading-3 font-medium">
{t('labels.nSelected', { count: selectedCount })}
</span>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';

import { Fragment, useMemo } from 'react';

import { DeleteDialog } from '@/app/components/ui/dialog/delete-dialog';
import { useT } from '@/lib/i18n/client';

Expand All @@ -20,18 +22,33 @@ export function DocumentDeleteDialog({
}: DocumentDeleteDialogProps) {
const { t: tDocuments } = useT('documents');

const displayName = fileName ?? tDocuments('deleteFile.thisDocument');

const description = useMemo(() => {
const raw = tDocuments('deleteFile.confirmation', { name: '{name}' });
const parts = raw.split('{name}');
if (parts.length <= 1) return raw;
return (
<>
{parts.map((part, index) => (
<Fragment key={index}>
{part}
{index < parts.length - 1 && <strong>{displayName}</strong>}
</Fragment>
))}
</>
);
}, [tDocuments, displayName]);

return (
<DeleteDialog
open={open}
onOpenChange={onOpenChange}
title={tDocuments('deleteFile.title')}
description={tDocuments('deleteFile.confirmation', {
name: fileName ?? tDocuments('deleteFile.thisFile'),
})}
description={description}
deleteText={tDocuments('deleteFile.deleteButton')}
isDeleting={isLoading}
onDelete={onConfirmDelete}
warning={tDocuments('deleteFile.warning')}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { type Row } from '@tanstack/react-table';
import { ClipboardList } from 'lucide-react';
import { useMemo, useState, useCallback } from 'react';

import type { DocumentItem } from '@/types/documents';
import type { FilterConfig } from '@/app/components/ui/data-table/data-table-filters';
import type { DocumentItem, RagStatus } from '@/types/documents';

import { DataTable } from '@/app/components/ui/data-table/data-table';
import { useTeams } from '@/app/features/settings/teams/hooks/queries';
Expand Down Expand Up @@ -57,6 +58,11 @@ export function DocumentsTable({
}, [teams]);

const { selectedTeamId } = useTeamFilter();
const { t: tTables } = useT('tables');

const [selectedRagStatuses, setSelectedRagStatuses] = useState<string[]>([]);
const [selectedSources, setSelectedSources] = useState<string[]>([]);
const [selectedTeamIds, setSelectedTeamIds] = useState<string[]>([]);

const paginatedResult = useListDocumentsPaginated({
organizationId,
Expand All @@ -77,6 +83,89 @@ export function DocumentsTable({
}));
}, [folders]);

const ragStatusFilterMap: Record<string, RagStatus[]> = useMemo(
() => ({
indexed: ['completed'],
not_indexed: ['not_indexed'],
indexing: ['queued', 'running'],
failed: ['failed'],
stale: ['stale'],
}),
[],
);

const filterConfigs = useMemo<FilterConfig[]>(() => {
const configs: FilterConfig[] = [
{
key: 'ragStatus',
title: tTables('headers.ragStatus'),
options: [
{ value: 'indexed', label: tDocuments('filter.ragStatus.indexed') },
{
value: 'not_indexed',
label: tDocuments('filter.ragStatus.notIndexed'),
},
{
value: 'indexing',
label: tDocuments('filter.ragStatus.indexing'),
},
{ value: 'failed', label: tDocuments('filter.ragStatus.failed') },
{
value: 'stale',
label: tDocuments('filter.ragStatus.needsReindex'),
},
],
selectedValues: selectedRagStatuses,
onChange: setSelectedRagStatuses,
multiSelect: true,
},
{
key: 'source',
title: tTables('headers.source'),
options: [
{ value: 'upload', label: tDocuments('filter.source.upload') },
{ value: 'onedrive', label: tDocuments('filter.source.oneDrive') },
{
value: 'sharepoint',
label: tDocuments('filter.source.sharePoint'),
},
],
selectedValues: selectedSources,
onChange: setSelectedSources,
multiSelect: true,
},
];

if (teams && teams.length > 0) {
configs.push({
key: 'teams',
title: tTables('headers.teams'),
options: teams.map((team) => ({
value: team.id,
label: team.name,
})),
selectedValues: selectedTeamIds,
onChange: setSelectedTeamIds,
multiSelect: true,
});
}

return configs;
}, [
tTables,
tDocuments,
selectedRagStatuses,
selectedSources,
selectedTeamIds,
teams,
]);

const handleClearFilters = useCallback(() => {
setSelectedRagStatuses([]);
setSelectedSources([]);
setSelectedTeamIds([]);
}, []);

const filteredResults = useMemo(() => {
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Convex paginated query results match DocumentItem shape
let filtered = paginatedResult.results as DocumentItem[];
Expand All @@ -85,6 +174,27 @@ export function DocumentsTable({
(doc) => !doc.teamId || doc.teamId === selectedTeamId,
);
}
if (selectedRagStatuses.length > 0) {
const allowedStatuses = new Set(
selectedRagStatuses.flatMap((key) => ragStatusFilterMap[key] ?? []),
);
filtered = filtered.filter((doc) => {
const status = doc.ragStatus ?? 'not_indexed';
return allowedStatuses.has(status);
});
}
if (selectedSources.length > 0) {
const sourceSet = new Set(selectedSources);
filtered = filtered.filter(
(doc) => doc.sourceProvider && sourceSet.has(doc.sourceProvider),
);
}
if (selectedTeamIds.length > 0) {
const teamIdSet = new Set(selectedTeamIds);
filtered = filtered.filter(
(doc) => doc.teamId && teamIdSet.has(doc.teamId),
);
}
if (debouncedQuery) {
const filteredFolders = filterByTextSearch(folderRows, debouncedQuery, [
'name',
Expand All @@ -93,7 +203,16 @@ export function DocumentsTable({
return [...filteredFolders, ...filtered];
}
return [...folderRows, ...filtered];
}, [paginatedResult.results, selectedTeamId, debouncedQuery, folderRows]);
}, [
paginatedResult.results,
selectedTeamId,
selectedRagStatuses,
selectedSources,
selectedTeamIds,
ragStatusFilterMap,
debouncedQuery,
folderRows,
]);

const previewDocument = useMemo(() => {
if (!docId || !filteredResults.length) return null;
Expand Down Expand Up @@ -210,6 +329,10 @@ export function DocumentsTable({
},
placeholder: searchPlaceholder,
},
filters: {
configs: filterConfigs,
onClear: handleClearFilters,
},
getRowId: (row) => row.id,
approxRowCount: docCount,
});
Expand Down
Loading
Loading