From fcd3f63ffc9b5530f971afd1da45a90a02d7cb70 Mon Sep 17 00:00:00 2001 From: israel Date: Tue, 10 Mar 2026 20:10:11 +0100 Subject: [PATCH 1/8] feat(platform): documents table UI polish and filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add RAG status, source, and teams filter popover to documents table - Fix delete dialog to match Pencil design (bold filename, simplified copy) - Use abbreviated month format (ll LT) in date columns with timezone - Add customFormat prop to CopyableTimestamp component - Show source column as text labels instead of icons - Left-align size column - Fix filter button to show icon + "Filter" text label - Fix i18n interpolation bug ({{count}} → {count}) in nSelected translation - Remove uppercase from filter section titles - Update selected count badge to neutral gray style - Clean up design comments files --- design/comments.md | 15 --- .../comments/knowledge-import-button.png | Bin 14779 -> 0 bytes .../ui/data-display/copyable-timestamp.tsx | 10 +- .../ui/data-table/data-table-filters.tsx | 2 +- .../ui/dialog/view-dialog.stories.tsx | 2 +- .../components/ui/filters/filter-button.tsx | 8 +- .../ui/filters/filter-section.test.tsx | 7 +- .../components/ui/filters/filter-section.tsx | 4 +- .../components/document-delete-dialog.tsx | 11 +- .../documents/components/documents-table.tsx | 127 +++++++++++++++++- .../onedrive-import/onedrive-file-table.tsx | 6 +- .../hooks/use-documents-table-config.tsx | 48 ++----- services/platform/messages/en.json | 31 +++-- 13 files changed, 191 insertions(+), 80 deletions(-) delete mode 100644 design/comments.md delete mode 100644 design/images/comments/knowledge-import-button.png diff --git a/design/comments.md b/design/comments.md deleted file mode 100644 index 8d33f3264..000000000 --- a/design/comments.md +++ /dev/null @@ -1,15 +0,0 @@ -# Design comments - -## Knowledge / Documents page - -- [ ] "Import documents" dropdown button is missing — should show a button with "+ Import documents" text and a chevron-down, with a dropdown menu containing "From your device" and "New folder" options -- [ ] "Upload document" button context — the current button exists but lacks the dropdown menu state shown in the reference - ![reference](images/comments/knowledge-import-button.png) - -## Documents page — Upload progress (#726) - -- [ ] Need a design for the upload progress state inside the document upload dialog. Currently implemented with a basic progress bar and byte counter text, but needs a proper design pass. - - Progress bar showing upload percentage (byte-level, not just per-file) - - Text showing uploaded / total bytes (e.g. "12.4 MB / 54.2 MB") - - For multi-file uploads: file completion count (e.g. "2 / 5 files completed") - - States to cover: uploading single file, uploading multiple files, upload complete diff --git a/design/images/comments/knowledge-import-button.png b/design/images/comments/knowledge-import-button.png deleted file mode 100644 index d95b4bb5a23af9e941e2ca3c91e440b09954956e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14779 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 diff --git a/services/platform/app/components/ui/data-display/copyable-timestamp.tsx b/services/platform/app/components/ui/data-display/copyable-timestamp.tsx index df55bd362..831f07710 100644 --- a/services/platform/app/components/ui/data-display/copyable-timestamp.tsx +++ b/services/platform/app/components/ui/data-display/copyable-timestamp.tsx @@ -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 */ @@ -35,6 +37,7 @@ interface CopyableTimestampProps { export const CopyableTimestamp = React.memo(function CopyableTimestamp({ date, preset = 'short', + customFormat, className, emptyText = '—', alignRight = false, @@ -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 ( diff --git a/services/platform/app/components/ui/data-table/data-table-filters.tsx b/services/platform/app/components/ui/data-table/data-table-filters.tsx index deb1d0413..539dd4d4f 100644 --- a/services/platform/app/components/ui/data-table/data-table-filters.tsx +++ b/services/platform/app/components/ui/data-table/data-table-filters.tsx @@ -188,7 +188,7 @@ export function DataTableFilters({ } >
- + {t('labels.filters')} {totalActiveFilters > 0 && ( diff --git a/services/platform/app/components/ui/dialog/view-dialog.stories.tsx b/services/platform/app/components/ui/dialog/view-dialog.stories.tsx index 47598c188..1bee7513f 100644 --- a/services/platform/app/components/ui/dialog/view-dialog.stories.tsx +++ b/services/platform/app/components/ui/dialog/view-dialog.stories.tsx @@ -111,7 +111,7 @@ export const WithHeaderActions: Story = { project-proposal.pdf 2.4 MB January 10, 2024 - January 20, 2024 + January 20, 2024 diff --git a/services/platform/app/components/ui/filters/filter-button.tsx b/services/platform/app/components/ui/filters/filter-button.tsx index 67e3ffa73..191c8559d 100644 --- a/services/platform/app/components/ui/filters/filter-button.tsx +++ b/services/platform/app/components/ui/filters/filter-button.tsx @@ -1,4 +1,4 @@ -import { Filter } from 'lucide-react'; +import { ListFilter } from 'lucide-react'; import { Loader2Icon } from 'lucide-react'; import { ComponentProps } from 'react'; @@ -22,10 +22,9 @@ export function FilterButton({ return ( )} - )} {!isLoading && doc?.url && ( - +
+
+ +
+ +
)} ); diff --git a/services/platform/app/features/documents/components/document-preview-docx.tsx b/services/platform/app/features/documents/components/document-preview-docx.tsx index 90285acc6..3419b5a88 100644 --- a/services/platform/app/features/documents/components/document-preview-docx.tsx +++ b/services/platform/app/features/documents/components/document-preview-docx.tsx @@ -5,6 +5,7 @@ import { useCallback } from 'react'; import { useT } from '@/lib/i18n/client'; import { useDocxPreview } from '../hooks/use-document-preview'; +import { PreviewPane } from './preview-pane'; interface DocumentPreviewDocxProps { url: string; @@ -22,7 +23,7 @@ export function DocumentPreviewDocx({ url }: DocumentPreviewDocxProps) { ); return ( -
+ {isLoading && (
{t('preview.loading')} @@ -39,6 +40,6 @@ export function DocumentPreviewDocx({ url }: DocumentPreviewDocxProps) { className="prose mx-auto aspect-[1/1.4] w-full max-w-2xl" /> )} -
+
); } diff --git a/services/platform/app/features/documents/components/document-preview-image.tsx b/services/platform/app/features/documents/components/document-preview-image.tsx new file mode 100644 index 000000000..37a9cca18 --- /dev/null +++ b/services/platform/app/features/documents/components/document-preview-image.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { useCallback, useState } from 'react'; + +import { ZoomPanViewer } from '@/app/components/ui/data-display/zoom-pan-viewer'; +import { Skeleton } from '@/app/components/ui/feedback/skeleton'; +import { Center } from '@/app/components/ui/layout/layout'; +import { Text } from '@/app/components/ui/typography/text'; +import { useT } from '@/lib/i18n/client'; + +import { PreviewPane } from './preview-pane'; + +interface DocumentPreviewImageProps { + url: string; + fileName?: string; +} + +export function DocumentPreviewImage({ + url, + fileName, +}: DocumentPreviewImageProps) { + const { t } = useT('documents'); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); + + const handleLoad = useCallback(() => { + setIsLoading(false); + }, []); + + const handleError = useCallback(() => { + setIsLoading(false); + setHasError(true); + }, []); + + if (hasError) { + return ( + + + {t('preview.failedToLoad')} + + + ); + } + + return ( + + {isLoading && ( +
+ +
+ )} + +
+ ); +} diff --git a/services/platform/app/features/documents/components/document-preview-pdf.tsx b/services/platform/app/features/documents/components/document-preview-pdf.tsx index 665ad2714..7a8b8980f 100644 --- a/services/platform/app/features/documents/components/document-preview-pdf.tsx +++ b/services/platform/app/features/documents/components/document-preview-pdf.tsx @@ -6,6 +6,8 @@ import React, { useReducer, useEffect, useRef, useCallback } from 'react'; import { HStack } from '@/app/components/ui/layout/layout'; import { useT } from '@/lib/i18n/client'; +import { PreviewPane } from './preview-pane'; + interface PageViewport { width: number; height: number; @@ -285,7 +287,7 @@ export const DocumentPreviewPDF = ({ url }: { url: string }) => { return ( <> -
+ {/* Canvas */} { {t('preview.loading')}
)} -
+ ); }; diff --git a/services/platform/app/features/documents/components/document-preview-text.tsx b/services/platform/app/features/documents/components/document-preview-text.tsx index 0311f8d28..31c6eb89d 100644 --- a/services/platform/app/features/documents/components/document-preview-text.tsx +++ b/services/platform/app/features/documents/components/document-preview-text.tsx @@ -12,6 +12,7 @@ import { } from '@/lib/utils/text-file-types'; import { useTextPreview } from '../hooks/use-document-preview'; +import { PreviewPane } from './preview-pane'; interface DocumentPreviewTextProps { url: string; @@ -58,7 +59,7 @@ export function DocumentPreviewText({ ); return ( -
+ {isLoading && ( {t('preview.loading')} @@ -87,6 +88,6 @@ export function DocumentPreviewText({
))} -
+ ); } diff --git a/services/platform/app/features/documents/components/document-preview-xlsx.tsx b/services/platform/app/features/documents/components/document-preview-xlsx.tsx index 25c50b77c..b803c9b93 100644 --- a/services/platform/app/features/documents/components/document-preview-xlsx.tsx +++ b/services/platform/app/features/documents/components/document-preview-xlsx.tsx @@ -5,6 +5,7 @@ import { useCallback } from 'react'; import { useT } from '@/lib/i18n/client'; import { useXlsxPreview } from '../hooks/use-document-preview'; +import { PreviewPane } from './preview-pane'; interface DocumentPreviewXlsxProps { url: string; @@ -22,7 +23,7 @@ export function DocumentPreviewXlsx({ url }: DocumentPreviewXlsxProps) { ); return ( -
+ {isLoading && (
{t('preview.loading')} @@ -39,6 +40,6 @@ export function DocumentPreviewXlsx({ url }: DocumentPreviewXlsxProps) { className="[&_td]:border-border [&_table]:bg-background text-foreground max-w-none [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:px-3 [&_td]:py-2 [&_td]:align-top [&_th]:text-left [&_tr]:border-b" /> )} -
+
); } diff --git a/services/platform/app/features/documents/components/document-preview.tsx b/services/platform/app/features/documents/components/document-preview.tsx index 0c41eec35..8d0a019bf 100644 --- a/services/platform/app/features/documents/components/document-preview.tsx +++ b/services/platform/app/features/documents/components/document-preview.tsx @@ -57,6 +57,27 @@ const DocumentPreviewText = lazyComponent( loading: () => , }, ); +const DocumentPreviewImage = lazyComponent( + () => + import('./document-preview-image').then((m) => ({ + default: m.DocumentPreviewImage, + })), + { + loading: () => , + }, +); + +const IMAGE_EXTENSIONS: ReadonlySet = new Set([ + 'JPG', + 'JPEG', + 'PNG', + 'GIF', + 'WEBP', + 'SVG', + 'BMP', + 'ICO', + 'AVIF', +]); export interface DocumentPreviewProps { url: string; @@ -124,6 +145,10 @@ export function DocumentPreview({ url, fileName }: DocumentPreviewProps) { return ; } + if (IMAGE_EXTENSIONS.has(extension)) { + return ; + } + if (isTextBasedFile(fileName || url)) { return ; } diff --git a/services/platform/app/features/documents/components/document-row-actions.tsx b/services/platform/app/features/documents/components/document-row-actions.tsx index 3577c2b8a..4c962e875 100644 --- a/services/platform/app/features/documents/components/document-row-actions.tsx +++ b/services/platform/app/features/documents/components/document-row-actions.tsx @@ -26,7 +26,7 @@ interface DocumentRowActionsProps { syncConfigId?: string; isDirectlySelected?: boolean; sourceMode?: StorageSourceMode; - teamId?: string | null; + teamIds?: string[]; onFolderDeleted?: () => void; } @@ -37,7 +37,7 @@ export function DocumentRowActions({ syncConfigId, isDirectlySelected, sourceMode, - teamId, + teamIds, onFolderDeleted, }: DocumentRowActionsProps) { const { t: tDocuments } = useT('documents'); @@ -199,7 +199,7 @@ export function DocumentRowActions({ onOpenChange={dialogs.setOpen.teamTags} documentId={documentId} documentName={name} - currentTeamId={teamId} + currentTeamIds={teamIds} /> ); diff --git a/services/platform/app/features/documents/components/document-team-tags-dialog.tsx b/services/platform/app/features/documents/components/document-team-tags-dialog.tsx index b4bc5313b..71a64a26f 100644 --- a/services/platform/app/features/documents/components/document-team-tags-dialog.tsx +++ b/services/platform/app/features/documents/components/document-team-tags-dialog.tsx @@ -1,18 +1,20 @@ 'use client'; -import { Users } from 'lucide-react'; -import { useState, useMemo } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { Settings, Users } from 'lucide-react'; +import { useCallback, useMemo, useState } from 'react'; import { Dialog } from '@/app/components/ui/dialog/dialog'; import { EmptyState } from '@/app/components/ui/feedback/empty-state'; -import { Select } from '@/app/components/ui/forms/select'; -import { Stack } from '@/app/components/ui/layout/layout'; +import { Checkbox } from '@/app/components/ui/forms/checkbox'; import { Button } from '@/app/components/ui/primitives/button'; import { Text } from '@/app/components/ui/typography/text'; import { useTeams } from '@/app/features/settings/teams/hooks/queries'; +import { useOrganizationId } from '@/app/hooks/use-organization-id'; import { toast } from '@/app/hooks/use-toast'; import { toId } from '@/convex/lib/type_cast_helpers'; import { useT } from '@/lib/i18n/client'; +import { cn } from '@/lib/utils/cn'; import { useUpdateDocument } from '../hooks/mutations'; @@ -21,11 +23,9 @@ interface DocumentTeamDialogProps { onOpenChange: (open: boolean) => void; documentId: string; documentName?: string | null; - currentTeamId?: string | null; + currentTeamIds?: string[]; } -const ORG_WIDE_VALUE = '__org_wide__'; - /** * Internal content component containing all hooks. * IMPORTANT: This component must only be rendered when the dialog is open. @@ -37,46 +37,54 @@ function DocumentTeamDialogContent({ onOpenChange, documentId, documentName, - currentTeamId, + currentTeamIds, }: DocumentTeamDialogProps) { const { t: tDocuments } = useT('documents'); const { t: tCommon } = useT('common'); + const navigate = useNavigate(); + const organizationId = useOrganizationId(); - const [selectedValue, setSelectedValue] = useState( - () => currentTeamId ?? ORG_WIDE_VALUE, + const [selectedTeamIds, setSelectedTeamIds] = useState>( + () => new Set(currentTeamIds ?? []), ); const [isSubmitting, setIsSubmitting] = useState(false); const updateDocument = useUpdateDocument(); const { teams, isLoading } = useTeams(); - const teamOptions = useMemo(() => { - const items = [ - { value: ORG_WIDE_VALUE, label: tDocuments('teamTags.orgWide') }, - ]; - if (teams) { - for (const team of teams) { - items.push({ value: team.id, label: team.name }); - } - } - return items; - }, [teams, tDocuments]); + const hasTeams = teams && teams.length > 0; - const handleClose = () => { + const handleClose = useCallback(() => { if (!isSubmitting) { onOpenChange(false); } - }; + }, [isSubmitting, onOpenChange]); + + const isOrgWide = selectedTeamIds.size === 0; + + const handleSelectOrgWide = useCallback(() => { + setSelectedTeamIds(new Set()); + }, []); - const handleSubmit = async () => { + const handleToggleTeam = useCallback((teamId: string) => { + setSelectedTeamIds((prev) => { + const next = new Set(prev); + if (next.has(teamId)) { + next.delete(teamId); + } else { + next.add(teamId); + } + return next; + }); + }, []); + + const handleSubmit = useCallback(async () => { setIsSubmitting(true); try { - const newTeamId = selectedValue === ORG_WIDE_VALUE ? null : selectedValue; - await updateDocument.mutateAsync({ documentId: toId<'documents'>(documentId), - teamId: newTeamId, + teamIds: [...selectedTeamIds], }); toast({ @@ -94,12 +102,16 @@ function DocumentTeamDialogContent({ } finally { setIsSubmitting(false); } - }; + }, [documentId, selectedTeamIds, updateDocument, onOpenChange, tDocuments]); const hasChanges = useMemo(() => { - const currentValue = currentTeamId ?? ORG_WIDE_VALUE; - return currentValue !== selectedValue; - }, [currentTeamId, selectedValue]); + const currentSet = new Set(currentTeamIds ?? []); + if (currentSet.size !== selectedTeamIds.size) return true; + for (const id of currentSet) { + if (!selectedTeamIds.has(id)) return true; + } + return false; + }, [currentTeamIds, selectedTeamIds]); const displayName = useMemo(() => { if (!documentName) return ''; @@ -107,60 +119,126 @@ function DocumentTeamDialogContent({ return parts[parts.length - 1] || documentName; }, [documentName]); + const handleGoToSettings = useCallback(() => { + if (!organizationId) return; + onOpenChange(false); + void navigate({ + to: '/dashboard/$id/settings/teams', + params: { id: organizationId }, + }); + }, [organizationId, onOpenChange, navigate]); + return ( - - - + hasTeams ? ( + <> + + + + ) : undefined } > - - {documentName && ( - - {tDocuments('teamTags.description', { name: displayName })} + {isLoading ? ( +
+ + {tCommon('actions.loading')} - )} - {isLoading ? ( -
- - {tCommon('actions.loading')} - +
+ ) : !hasTeams ? ( + +