From 8c6753c18863d8ad84df82d81ace0b861824fdbf Mon Sep 17 00:00:00 2001 From: lizerk Date: Fri, 6 Mar 2026 12:23:11 +0300 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D1=88=D0=B8=D1=85=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Удален DocumentStatusBadge (больше нет статусов) - Удален DocumentWorkflowButtons (нет FSM workflow) - Удален DocumentVersionHistory (дубль, есть другая реализация) - Удален AdvancedSearch (заменен на фильтры) - Удалена папка versions/ с компонентами истории версий --- .../documents/DocumentStatusBadge.tsx | 60 -- .../documents/DocumentVersionHistory.tsx | 196 ------- .../documents/DocumentWorkflowButtons.tsx | 168 ------ .../documents/search/AdvancedSearch.tsx | 521 ------------------ .../src/components/documents/search/index.ts | 1 - .../versions/DocumentActivityTimeline.tsx | 114 ---- .../versions/DocumentVersionHistory.tsx | 190 ------- .../components/documents/versions/index.ts | 2 - 8 files changed, 1252 deletions(-) delete mode 100644 frontend/src/components/documents/DocumentStatusBadge.tsx delete mode 100644 frontend/src/components/documents/DocumentVersionHistory.tsx delete mode 100644 frontend/src/components/documents/DocumentWorkflowButtons.tsx delete mode 100644 frontend/src/components/documents/search/AdvancedSearch.tsx delete mode 100644 frontend/src/components/documents/search/index.ts delete mode 100644 frontend/src/components/documents/versions/DocumentActivityTimeline.tsx delete mode 100644 frontend/src/components/documents/versions/DocumentVersionHistory.tsx delete mode 100644 frontend/src/components/documents/versions/index.ts diff --git a/frontend/src/components/documents/DocumentStatusBadge.tsx b/frontend/src/components/documents/DocumentStatusBadge.tsx deleted file mode 100644 index 402d81fb..00000000 --- a/frontend/src/components/documents/DocumentStatusBadge.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import type { DocumentStatus } from "@/types/api"; - -interface DocumentStatusBadgeProps { - status: string; // human-readable: "Черновик", "На рассмотрении", etc. - statusCode: DocumentStatus; // machine-readable: "draft", "in_review", etc. - className?: string; -} - -const statusConfig: Record = { - draft: { - label: "Черновик", - color: "text-gray-700", - bgColor: "bg-gray-50", - ringColor: "ring-gray-200", - }, - in_review: { - label: "На рассмотрении", - color: "text-cyan-700", - bgColor: "bg-cyan-50", - ringColor: "ring-cyan-200", - }, - approved: { - label: "Утверждено", - color: "text-green-700", - bgColor: "bg-green-50", - ringColor: "ring-green-200", - }, - published: { - label: "Опубликовано", - color: "text-blue-700", - bgColor: "bg-blue-50", - ringColor: "ring-blue-200", - }, - archived: { - label: "В архиве", - color: "text-gray-600", - bgColor: "bg-gray-100", - ringColor: "ring-gray-300", - }, - rejected: { - label: "Отклонено", - color: "text-red-700", - bgColor: "bg-red-50", - ringColor: "ring-red-200", - }, -}; - -export function DocumentStatusBadge({ status, statusCode, className = "" }: DocumentStatusBadgeProps) { - const config = statusConfig[statusCode] || statusConfig.draft; - - return ( - - {status || config.label} - - ); -} diff --git a/frontend/src/components/documents/DocumentVersionHistory.tsx b/frontend/src/components/documents/DocumentVersionHistory.tsx deleted file mode 100644 index 70cc1e52..00000000 --- a/frontend/src/components/documents/DocumentVersionHistory.tsx +++ /dev/null @@ -1,196 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { History, RotateCcw, User as UserIcon, Clock } from "lucide-react"; -import { apiClient } from "@/lib/api"; -import { toast } from "sonner"; - -interface Version { - id: number; - revision_id: number; - date_created: string; - user: { - id: number; - full_name: string; - } | null; - comment: string; - data: any; -} - -interface DocumentVersionHistoryProps { - documentId: number; - onRevert?: () => void; -} - -export function DocumentVersionHistory({ documentId, onRevert }: DocumentVersionHistoryProps) { - const [versions, setVersions] = useState([]); - const [loading, setLoading] = useState(true); - const [reverting, setReverting] = useState(false); - - useEffect(() => { - loadVersions(); - }, [documentId]); - - const loadVersions = async () => { - try { - setLoading(true); - const response = await apiClient.getDocumentVersions(documentId); - const data = Array.isArray(response) ? response : (response.results || []); - setVersions(data); - } catch (error) { - console.error("Ошибка загрузки версий:", error); - toast.error("Не удалось загрузить историю версий"); - } finally { - setLoading(false); - } - }; - - const handleRevert = async (versionId: number) => { - const comment = prompt("Укажите причину отката (опционально):"); - if (comment === null) return; // User cancelled - - setReverting(true); - try { - await apiClient.revertDocumentToVersion(documentId, versionId); - toast.success("Документ откачен к выбранной версии"); - if (onRevert) { - onRevert(); - } - await loadVersions(); - } catch (error) { - console.error("Ошибка отката:", error); - toast.error("Не удалось откатить документ"); - } finally { - setReverting(false); - } - }; - - const formatDate = (dateString: string) => { - const date = new Date(dateString); - return date.toLocaleString("ru-RU", { - day: "2-digit", - month: "short", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }); - }; - - const getChangedFields = (versionData: any): string[] => { - if (!versionData || !versionData.data) return []; - const fields = Object.keys(versionData.data); - return fields.filter(f => !['id', 'created_at', 'updated_at'].includes(f)); - }; - - if (loading) { - return ( -
-
-
- ); - } - - if (versions.length === 0) { - return ( -
- -

- История версий пока пуста -

-
- ); - } - - return ( -
-
- -

- История изменений ({versions.length}) -

-
- -
- {versions.map((version, index) => { - const isLatest = index === 0; - const changedFields = getChangedFields(version); - - return ( -
-
-
- {/* Header */} -
- {isLatest && ( - - Текущая - - )} -
- - {formatDate(version.date_created)} -
-
- - {/* User */} - {version.user && ( -
- - - {version.user.full_name} - -
- )} - - {/* Comment */} - {version.comment && ( -

{version.comment}

- )} - - {/* Changed fields */} - {changedFields.length > 0 && ( -
- {changedFields.slice(0, 5).map((field) => ( - - {field} - - ))} - {changedFields.length > 5 && ( - - +{changedFields.length - 5} ещё - - )} -
- )} -
- - {/* Revert button */} - {!isLatest && ( - - )} -
-
- ); - })} -
-
- ); -} diff --git a/frontend/src/components/documents/DocumentWorkflowButtons.tsx b/frontend/src/components/documents/DocumentWorkflowButtons.tsx deleted file mode 100644 index baa48959..00000000 --- a/frontend/src/components/documents/DocumentWorkflowButtons.tsx +++ /dev/null @@ -1,168 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { - Send, - CheckCircle, - XCircle, - Globe, - RotateCcw, - Archive, - ArchiveRestore -} from "lucide-react"; -import { apiClient } from "@/lib/api"; -import { toast } from "sonner"; -import type { DocumentStatus } from "@/types/api"; - -interface DocumentWorkflowButtonsProps { - documentId: number; - currentStatus: DocumentStatus; - onStatusChange?: () => void; -} - -interface WorkflowAction { - label: string; - icon: React.ReactNode; - action: () => Promise; - confirmMessage?: string; - successMessage: string; - variant: "primary" | "success" | "danger" | "secondary"; -} - -export function DocumentWorkflowButtons({ - documentId, - currentStatus, - onStatusChange, -}: DocumentWorkflowButtonsProps) { - const [isLoading, setIsLoading] = useState(false); - - const handleAction = async (action: WorkflowAction) => { - if (action.confirmMessage && !window.confirm(action.confirmMessage)) { - return; - } - - setIsLoading(true); - try { - await action.action(); - toast.success(action.successMessage); - if (onStatusChange) { - onStatusChange(); - } - } catch (err) { - console.error("Ошибка изменения статуса:", err); - toast.error("Не удалось изменить статус документа"); - } finally { - setIsLoading(false); - } - }; - - const getAvailableActions = (): WorkflowAction[] => { - const actions: WorkflowAction[] = []; - - switch (currentStatus) { - case "draft": - actions.push({ - label: "Отправить на рассмотрение", - icon: , - action: () => apiClient.submitDocumentForReview(documentId), - successMessage: "Документ отправлен на рассмотрение", - variant: "primary", - }); - break; - - case "in_review": - actions.push( - { - label: "Утвердить", - icon: , - action: () => apiClient.approveDocument(documentId), - confirmMessage: "Вы уверены, что хотите утвердить этот документ?", - successMessage: "Документ утвержден", - variant: "success", - }, - { - label: "Отклонить", - icon: , - action: () => apiClient.rejectDocument(documentId), - confirmMessage: "Вы уверены, что хотите отклонить этот документ?", - successMessage: "Документ отклонен", - variant: "danger", - }, - { - label: "Вернуть в черновик", - icon: , - action: () => apiClient.returnDocumentToDraft(documentId), - successMessage: "Документ возвращен в черновик", - variant: "secondary", - } - ); - break; - - case "approved": - actions.push({ - label: "Опубликовать", - icon: , - action: () => apiClient.publishDocument(documentId), - confirmMessage: "Опубликовать этот документ для всех пользователей?", - successMessage: "Документ опубликован", - variant: "primary", - }); - break; - - case "published": - actions.push({ - label: "Архивировать", - icon: , - action: () => apiClient.archiveDocument(documentId), - confirmMessage: "Архивировать этот документ?", - successMessage: "Документ архивирован", - variant: "secondary", - }); - break; - - case "archived": - actions.push({ - label: "Разархивировать", - icon: , - action: () => apiClient.unarchiveDocument(documentId), - successMessage: "Документ разархивирован", - variant: "primary", - }); - break; - } - - return actions; - }; - - const actions = getAvailableActions(); - - if (actions.length === 0) { - return null; - } - - const variantClasses = { - primary: "bg-sky-600 hover:bg-sky-700 text-white", - success: "bg-green-600 hover:bg-green-700 text-white", - danger: "bg-red-600 hover:bg-red-700 text-white", - secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800", - }; - - return ( -
- {actions.map((action, index) => ( - - ))} -
- ); -} diff --git a/frontend/src/components/documents/search/AdvancedSearch.tsx b/frontend/src/components/documents/search/AdvancedSearch.tsx deleted file mode 100644 index 26f23b57..00000000 --- a/frontend/src/components/documents/search/AdvancedSearch.tsx +++ /dev/null @@ -1,521 +0,0 @@ -"use client"; - -import { useState, useCallback, useEffect, useMemo } from "react"; -import { Search, X, Filter, SlidersHorizontal, Save, Clock, ChevronDown } from "lucide-react"; -import Fuse from "fuse.js"; - -export interface SearchFilters { - query: string; - documentTypes?: string[]; - dateFrom?: string; - dateTo?: string; - statuses?: string[]; - tags?: string[]; - authors?: string[]; - sortBy?: "relevance" | "date" | "title" | "author"; - sortOrder?: "asc" | "desc"; -} - -export interface SearchResult { - id: number; - title: string; - description: string; - content?: string; - type?: string; - status?: string; - uploaded_at: string; - uploaded_by?: string; - tags?: string[]; - score?: number; - highlights?: string[]; -} - -interface AdvancedSearchProps { - onSearch: (filters: SearchFilters) => void; - results?: SearchResult[]; - isLoading?: boolean; - availableTypes?: Array<{ id: string; name: string }>; - availableStatuses?: Array<{ id: string; name: string }>; - availableTags?: Array<{ id: string; name: string }>; - availableAuthors?: Array<{ id: string; name: string }>; -} - -const DEFAULT_FILTERS: SearchFilters = { - query: "", - documentTypes: [], - dateFrom: "", - dateTo: "", - statuses: [], - tags: [], - authors: [], - sortBy: "relevance", - sortOrder: "desc", -}; - -export function AdvancedSearch({ - onSearch, - results = [], - isLoading = false, - availableTypes = [], - availableStatuses = [], - availableTags = [], - availableAuthors = [], -}: AdvancedSearchProps) { - const [filters, setFilters] = useState(DEFAULT_FILTERS); - const [showFilters, setShowFilters] = useState(false); - const [savedSearches, setSavedSearches] = useState>( - [] - ); - const [showSavedSearches, setShowSavedSearches] = useState(false); - const [saveSearchName, setSaveSearchName] = useState(""); - - // Загрузка сохраненных поисков из localStorage - useEffect(() => { - const saved = localStorage.getItem("savedSearches"); - if (saved) { - try { - setSavedSearches(JSON.parse(saved)); - } catch (e) { - console.error("Error loading saved searches:", e); - } - } - }, []); - - // Обновление фильтра - const updateFilter = useCallback( - (key: K, value: SearchFilters[K]) => { - setFilters((prev) => ({ ...prev, [key]: value })); - }, - [] - ); - - // Выполнение поиска - const handleSearch = useCallback(() => { - onSearch(filters); - }, [filters, onSearch]); - - // Сброс фильтров - const resetFilters = useCallback(() => { - setFilters(DEFAULT_FILTERS); - onSearch(DEFAULT_FILTERS); - }, [onSearch]); - - // Сохранение поиска - const saveSearch = useCallback(() => { - if (!saveSearchName.trim()) return; - - const newSearch = { - name: saveSearchName.trim(), - filters: { ...filters }, - }; - - const updated = [...savedSearches, newSearch]; - setSavedSearches(updated); - localStorage.setItem("savedSearches", JSON.stringify(updated)); - setSaveSearchName(""); - setShowSavedSearches(false); - }, [saveSearchName, filters, savedSearches]); - - // Загрузка сохраненного поиска - const loadSearch = useCallback( - (search: { name: string; filters: SearchFilters }) => { - setFilters(search.filters); - onSearch(search.filters); - setShowSavedSearches(false); - }, - [onSearch] - ); - - // Удаление сохраненного поиска - const deleteSavedSearch = useCallback( - (index: number) => { - const updated = savedSearches.filter((_, i) => i !== index); - setSavedSearches(updated); - localStorage.setItem("savedSearches", JSON.stringify(updated)); - }, - [savedSearches] - ); - - // Fuzzy search для подсветки - const fuse = useMemo(() => { - if (!results.length || !filters.query) return null; - - return new Fuse(results, { - keys: ["title", "description", "content"], - includeScore: true, - includeMatches: true, - threshold: 0.4, - }); - }, [results, filters.query]); - - // Подсветка совпадений - const highlightText = (text: string, query: string) => { - if (!query) return text; - - const regex = new RegExp(`(${query})`, "gi"); - const parts = text.split(regex); - - return parts.map((part, index) => - regex.test(part) ? ( - - {part} - - ) : ( - {part} - ) - ); - }; - - // Активные фильтры (для отображения badges) - const activeFiltersCount = useMemo(() => { - let count = 0; - if (filters.documentTypes && filters.documentTypes.length > 0) count++; - if (filters.dateFrom) count++; - if (filters.dateTo) count++; - if (filters.statuses && filters.statuses.length > 0) count++; - if (filters.tags && filters.tags.length > 0) count++; - if (filters.authors && filters.authors.length > 0) count++; - return count; - }, [filters]); - - return ( -
- {/* Поисковая строка */} -
-
- - updateFilter("query", e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - placeholder="Поиск документов..." - className="w-full rounded-lg border border-gray-300 py-2.5 pl-10 pr-10 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-100" - /> - {filters.query && ( - - )} -
- - - - - - -
- - {/* Сохраненные поиски */} - {showSavedSearches && ( -
-
-

Сохраненные поиски

- -
- - {savedSearches.length > 0 ? ( -
- {savedSearches.map((search, index) => ( -
- - -
- ))} -
- ) : ( -

Нет сохраненных поисков

- )} - - {/* Сохранить текущий поиск */} -
- setSaveSearchName(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && saveSearch()} - placeholder="Название поиска..." - className="flex-1 rounded border border-gray-300 px-2 py-1 text-sm focus:border-sky-500 focus:outline-none" - /> - -
-
- )} - - {/* Панель фильтров */} - {showFilters && ( -
-
-

Расширенные фильтры

- -
- -
- {/* Тип документа */} - {availableTypes.length > 0 && ( -
- - -
- )} - - {/* Статус */} - {availableStatuses.length > 0 && ( -
- - -
- )} - - {/* Теги */} - {availableTags.length > 0 && ( -
- - -
- )} - - {/* Дата от */} -
- - updateFilter("dateFrom", e.target.value)} - className="w-full rounded border border-gray-300 px-2 py-1 text-sm focus:border-sky-500 focus:outline-none" - /> -
- - {/* Дата до */} -
- - updateFilter("dateTo", e.target.value)} - className="w-full rounded border border-gray-300 px-2 py-1 text-sm focus:border-sky-500 focus:outline-none" - /> -
- - {/* Автор */} - {availableAuthors.length > 0 && ( -
- - -
- )} -
- - {/* Сортировка */} -
- - - - -
-
- )} - - {/* Результаты поиска */} - {results.length > 0 && ( -
-

- Найдено результатов: {results.length} -

- - {results.map((result) => ( -
-

- {highlightText(result.title, filters.query)} -

- {result.description && ( -

- {highlightText(result.description, filters.query)} -

- )} -
- {result.type && ( - {result.type} - )} - {result.status && ( - - {result.status} - - )} - {result.uploaded_by && Автор: {result.uploaded_by}} - {result.uploaded_at && ( - {new Date(result.uploaded_at).toLocaleDateString("ru-RU")} - )} -
-
- ))} -
- )} - - {/* Пустое состояние */} - {!isLoading && results.length === 0 && filters.query && ( -
- -

Ничего не найдено

-

Попробуйте изменить параметры поиска

-
- )} -
- ); -} diff --git a/frontend/src/components/documents/search/index.ts b/frontend/src/components/documents/search/index.ts deleted file mode 100644 index 619e105d..00000000 --- a/frontend/src/components/documents/search/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AdvancedSearch, type SearchFilters, type SearchResult } from "./AdvancedSearch"; diff --git a/frontend/src/components/documents/versions/DocumentActivityTimeline.tsx b/frontend/src/components/documents/versions/DocumentActivityTimeline.tsx deleted file mode 100644 index 32c35bf1..00000000 --- a/frontend/src/components/documents/versions/DocumentActivityTimeline.tsx +++ /dev/null @@ -1,114 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { DocumentActivity } from '@/types/api'; -import { apiClient } from '@/lib/api'; -import { formatDistanceToNow } from 'date-fns'; -import { ru } from 'date-fns/locale'; - -interface DocumentActivityTimelineProps { - documentId: number; -} - -export default function DocumentActivityTimeline({ documentId }: DocumentActivityTimelineProps) { - const [activities, setActivities] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - loadActivities(); - }, [documentId]); - - const loadActivities = async () => { - try { - setLoading(true); - setError(null); - const response = await apiClient.getDocumentActivity(documentId); - setActivities(response.results || response); - } catch (err: any) { - setError(err.message || 'Ошибка загрузки активности'); - console.error('Failed to load activity:', err); - } finally { - setLoading(false); - } - }; - - const getActionIcon = (action: string) => { - const actionLower = action.toLowerCase(); - if (actionLower.includes('created') || actionLower.includes('создан')) return 'bi-plus-circle-fill text-success'; - if (actionLower.includes('updated') || actionLower.includes('обновлен')) return 'bi-pencil-fill text-primary'; - if (actionLower.includes('deleted') || actionLower.includes('удалён')) return 'bi-trash-fill text-danger'; - if (actionLower.includes('comment') || actionLower.includes('комментарий')) return 'bi-chat-fill text-info'; - if (actionLower.includes('version') || actionLower.includes('версия')) return 'bi-clock-history text-warning'; - return 'bi-circle-fill text-secondary'; - }; - - if (loading) { - return ( -
-
- Загрузка... -
-
- ); - } - - if (error) { - return ( -
- {error} -
- ); - } - - if (activities.length === 0) { - return ( -
- История активности пуста -
- ); - } - - return ( -
-
Активность ({activities.length})
- -
- {activities.map((activity, index) => { - const timeAgo = formatDistanceToNow(new Date(activity.timestamp), { - addSuffix: true, - locale: ru, - }); - - return ( -
- {/* Иконка */} -
- -
- - {/* Содержимое */} -
-
-
-
-
- {activity.user} - · - {activity.action} -
- {timeAgo} -
- {activity.description && ( -
{activity.description}
- )} -
-
-
-
- ); - })} -
-
- ); -} diff --git a/frontend/src/components/documents/versions/DocumentVersionHistory.tsx b/frontend/src/components/documents/versions/DocumentVersionHistory.tsx deleted file mode 100644 index ff3b3059..00000000 --- a/frontend/src/components/documents/versions/DocumentVersionHistory.tsx +++ /dev/null @@ -1,190 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { DocumentVersion } from '@/types/api'; -import { apiClient } from '@/lib/api'; -import { formatDistanceToNow } from 'date-fns'; -import { ru } from 'date-fns/locale'; - -interface DocumentVersionHistoryProps { - documentId: number; - onRevert?: () => void; -} - -export default function DocumentVersionHistory({ - documentId, - onRevert, -}: DocumentVersionHistoryProps) { - const [versions, setVersions] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [reverting, setReverting] = useState(null); - const [expandedVersion, setExpandedVersion] = useState(null); - - useEffect(() => { - loadVersions(); - }, [documentId]); - - const loadVersions = async () => { - try { - setLoading(true); - setError(null); - const response = await apiClient.getDocumentVersions(documentId); - setVersions(response.results || response); - } catch (err: any) { - setError(err.message || 'Ошибка загрузки версий'); - console.error('Failed to load versions:', err); - } finally { - setLoading(false); - } - }; - - const handleRevert = async (versionId: number) => { - if (!confirm('Вернуться к этой версии документа? Текущее состояние будет сохранено как новая версия.')) { - return; - } - - try { - setReverting(versionId); - await apiClient.revertDocumentToVersion(documentId, versionId); - await loadVersions(); - if (onRevert) onRevert(); - } catch (err: any) { - alert(err.message || 'Ошибка при откате к версии'); - } finally { - setReverting(null); - } - }; - - const toggleExpanded = (versionId: number) => { - setExpandedVersion(expandedVersion === versionId ? null : versionId); - }; - - if (loading) { - return ( -
-
- Загрузка... -
-
- ); - } - - if (error) { - return ( -
- {error} -
- ); - } - - if (versions.length === 0) { - return ( -
- История версий пуста -
- ); - } - - return ( -
-
История версий ({versions.length})
- -
- {versions.map((version, index) => { - const isLatest = index === 0; - const timeAgo = formatDistanceToNow(new Date(version.created_at), { - addSuffix: true, - locale: ru, - }); - const hasChanges = version.changes && Object.keys(version.changes).length > 0; - const isExpanded = expandedVersion === version.id; - - return ( -
-
-
-
-
-
- Версия {version.version} - {isLatest && ( - Текущая - )} -
-
- {version.user} - · - {timeAgo} -
- {version.comment && ( -
{version.comment}
- )} -
- - {!isLatest && ( - - )} -
- - {/* Изменения */} - {hasChanges && ( -
- - - {isExpanded && ( -
- - - {Object.entries(version.changes).map(([field, value]) => ( - - - - - ))} - -
- {field}: - - - {typeof value === 'object' - ? JSON.stringify(value, null, 2) - : String(value)} - -
-
- )} -
- )} -
-
-
- ); - })} -
-
- ); -} diff --git a/frontend/src/components/documents/versions/index.ts b/frontend/src/components/documents/versions/index.ts deleted file mode 100644 index ca0ca881..00000000 --- a/frontend/src/components/documents/versions/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as DocumentVersionHistory } from './DocumentVersionHistory'; -export { default as DocumentActivityTimeline } from './DocumentActivityTimeline';