Rename the task-modal Documents tab to Artifacts and include task-scoped media outputs alongside task documents. - Add shared artifact media rendering for global and task-local artifact cards. - Surface image, video, audio, document, and generic artifacts in the task Artifacts tab. - Update tab labels, empty states, counts, translations, tests, docs, and release notes for the Artifacts wording. Files changed: .changeset/fn-6907-task-artifacts-tab.md | 5 + docs/dashboard-guide.md | 5 +- .../dashboard/app/components/ArtifactMedia.tsx | 55 +++++++ .../dashboard/app/components/DocumentsView.tsx | 47 +----- .../dashboard/app/components/TaskDetailModal.tsx | 3 +- .../dashboard/app/components/TaskDocumentsTab.css | 33 +++++ .../dashboard/app/components/TaskDocumentsTab.tsx | 109 ++++++++++++-- .../TaskDetailModal.attachments-and-tabs.test.tsx | 4 +- .../TaskDetailModal.definition-actions.test.tsx | 20 +-- .../components/__tests__/TaskDocumentsTab.test.tsx | 160 ++++++++++++++++++++- packages/i18n/locales/en/app.json | 16 ++- packages/i18n/locales/es/app.json | 16 ++- packages/i18n/locales/fr/app.json | 16 ++- packages/i18n/locales/ko/app.json | 16 ++- packages/i18n/locales/zh-CN/app.json | 16 ++- packages/i18n/locales/zh-TW/app.json | 16 ++- 16 files changed, 436 insertions(+), 101 deletions(-) Fusion-Task-Id: FN-6907 Fusion-Task-Lineage: a1318ed5-4d47-42f8-8b6c-fe91506e11fb
555 lines
22 KiB
TypeScript
555 lines
22 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FileText, ChevronDown, ChevronUp, Plus, Trash2, History } from "lucide-react";
|
|
import "./DocumentsView.css";
|
|
import "./TaskDocumentsTab.css";
|
|
import ReactMarkdown from "react-markdown";
|
|
import remarkGfm from "remark-gfm";
|
|
import type { ArtifactWithTask, Task, TaskDocument, TaskDocumentRevision } from "@fusion/core";
|
|
import { getErrorMessage } from "@fusion/core";
|
|
import type { ToastType } from "../hooks/useToast";
|
|
import {
|
|
fetchTaskDocuments,
|
|
fetchTaskDocumentRevisions,
|
|
putTaskDocument,
|
|
deleteTaskDocument,
|
|
artifactMediaUrl,
|
|
} from "../api";
|
|
import { useArtifacts } from "../hooks/useArtifacts";
|
|
import { LoadingSpinner } from "./LoadingSpinner";
|
|
import { ArtifactMedia, getArtifactTypeLabel } from "./ArtifactMedia";
|
|
|
|
// Document key validation: alphanumeric, hyphens, underscores, 1-64 chars
|
|
const DOCUMENT_KEY_REGEX = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
const MAX_CONTENT_PREVIEW = 200;
|
|
|
|
interface TaskDocumentsTabProps {
|
|
taskId: string;
|
|
addToast: (message: string, type?: ToastType) => void;
|
|
onTaskUpdated?: (task: Task) => void;
|
|
projectId?: string;
|
|
canEdit?: boolean;
|
|
}
|
|
|
|
function formatTimestamp(iso?: string): string {
|
|
if (!iso) return "";
|
|
return new Date(iso).toLocaleString();
|
|
}
|
|
|
|
function getContentPreview(content: string, maxLength: number = MAX_CONTENT_PREVIEW): string {
|
|
if (content.length <= maxLength) return content;
|
|
return content.substring(0, maxLength) + "…";
|
|
}
|
|
|
|
function formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) {
|
|
return `${bytes} B`;
|
|
}
|
|
|
|
if (bytes < 1024 * 1024) {
|
|
return `${(bytes / 1024).toFixed(bytes >= 10 * 1024 ? 0 : 1)} KB`;
|
|
}
|
|
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
interface TaskArtifactCardProps {
|
|
artifact: ArtifactWithTask;
|
|
projectId?: string;
|
|
}
|
|
|
|
function TaskArtifactCard({ artifact, projectId }: TaskArtifactCardProps) {
|
|
const { t } = useTranslation("app");
|
|
const mediaUrl = artifactMediaUrl(artifact.id, projectId);
|
|
const typeLabel = getArtifactTypeLabel(t, artifact.type);
|
|
const preview = artifact.content ? getContentPreview(artifact.content, 320) : artifact.description;
|
|
const title = artifact.title || t("documents.untitledArtifact", "Untitled artifact");
|
|
|
|
return (
|
|
<article className="document-card documents-artifact-card" aria-label={t("documents.artifactCardLabel", "Artifact {{title}}", { title })}>
|
|
<div className="documents-artifact-preview">
|
|
<ArtifactMedia artifact={artifact} mediaUrl={mediaUrl} title={title} preview={preview} t={t} />
|
|
</div>
|
|
<div className="documents-artifact-body">
|
|
<div className="documents-artifact-header">
|
|
<span className="documents-artifact-type-badge">{typeLabel}</span>
|
|
<span className="documents-artifact-author">{artifact.authorId}</span>
|
|
</div>
|
|
<h5 className="documents-artifact-title">{title}</h5>
|
|
{artifact.description && <p className="documents-artifact-description">{artifact.description}</p>}
|
|
<div className="documents-artifact-meta">
|
|
<span>{formatTimestamp(artifact.createdAt)}</span>
|
|
{artifact.sizeBytes !== undefined && <span>{formatFileSize(artifact.sizeBytes)}</span>}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
export function TaskDocumentsTab({
|
|
taskId,
|
|
addToast,
|
|
onTaskUpdated: _onTaskUpdated,
|
|
projectId,
|
|
canEdit = false,
|
|
}: TaskDocumentsTabProps) {
|
|
const { t } = useTranslation("app");
|
|
const [documents, setDocuments] = useState<TaskDocument[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [expandedDocKey, setExpandedDocKey] = useState<string | null>(null);
|
|
const [expandedContent, setExpandedContent] = useState("");
|
|
const [editingDocKey, setEditingDocKey] = useState<string | null>(null);
|
|
const [editContent, setEditContent] = useState("");
|
|
const [showHistory, setShowHistory] = useState<string | null>(null);
|
|
const [revisions, setRevisions] = useState<TaskDocumentRevision[]>([]);
|
|
const [loadingRevisions, setLoadingRevisions] = useState(false);
|
|
const [showCreateForm, setShowCreateForm] = useState(false);
|
|
const [newDocKey, setNewDocKey] = useState("");
|
|
const [newDocContent, setNewDocContent] = useState("");
|
|
const [saving, setSaving] = useState(false);
|
|
const [deletingKey, setDeletingKey] = useState<string | null>(null);
|
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
|
const [renderMarkdown, setRenderMarkdown] = useState(false);
|
|
const { artifacts, loading: artifactsLoading, error: artifactsError } = useArtifacts({ projectId, taskId });
|
|
|
|
const loadDocuments = useCallback(async () => {
|
|
try {
|
|
const docs = await fetchTaskDocuments(taskId, projectId);
|
|
setDocuments(docs);
|
|
} catch (error) {
|
|
addToast(getErrorMessage(error) || t("taskDocuments.failedToLoad", "Failed to load documents"), "error");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [taskId, projectId, addToast]);
|
|
|
|
useEffect(() => {
|
|
void loadDocuments();
|
|
}, [loadDocuments]);
|
|
|
|
useEffect(() => {
|
|
if (artifactsError) {
|
|
addToast(artifactsError || t("taskDocuments.failedToLoadArtifacts", "Failed to load artifacts"), "error");
|
|
}
|
|
}, [addToast, artifactsError, t]);
|
|
|
|
async function handleExpandDocument(doc: TaskDocument) {
|
|
if (expandedDocKey === doc.key) {
|
|
setExpandedDocKey(null);
|
|
setExpandedContent("");
|
|
setEditingDocKey(null);
|
|
setEditContent("");
|
|
setShowHistory(null);
|
|
setRevisions([]);
|
|
setRenderMarkdown(false);
|
|
} else {
|
|
setExpandedDocKey(doc.key);
|
|
setExpandedContent(doc.content);
|
|
setEditingDocKey(null);
|
|
setEditContent("");
|
|
setShowHistory(null);
|
|
setRevisions([]);
|
|
setRenderMarkdown(false);
|
|
}
|
|
}
|
|
|
|
async function handleToggleHistory(docKey: string) {
|
|
if (showHistory === docKey) {
|
|
setShowHistory(null);
|
|
setRevisions([]);
|
|
} else {
|
|
setShowHistory(docKey);
|
|
setLoadingRevisions(true);
|
|
try {
|
|
const revs = await fetchTaskDocumentRevisions(taskId, docKey, projectId);
|
|
setRevisions(revs);
|
|
} catch (error) {
|
|
addToast(getErrorMessage(error) || t("taskDocuments.failedToLoadRevisions", "Failed to load revisions"), "error");
|
|
} finally {
|
|
setLoadingRevisions(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleStartEdit() {
|
|
if (expandedDocKey) {
|
|
setEditingDocKey(expandedDocKey);
|
|
setEditContent(expandedContent);
|
|
}
|
|
}
|
|
|
|
function handleCancelEdit() {
|
|
setEditingDocKey(null);
|
|
setEditContent("");
|
|
}
|
|
|
|
async function handleSaveEdit() {
|
|
if (!editingDocKey || !editContent.trim()) return;
|
|
setSaving(true);
|
|
try {
|
|
await putTaskDocument(taskId, editingDocKey, editContent, {}, projectId);
|
|
setEditingDocKey(null);
|
|
setEditContent("");
|
|
await loadDocuments();
|
|
// Refresh expanded content
|
|
const updated = documents.find((d) => d.key === editingDocKey);
|
|
if (updated) {
|
|
setExpandedContent(updated.content);
|
|
}
|
|
addToast(t("taskDocuments.saved", "Document saved"), "success");
|
|
} catch (error) {
|
|
addToast(getErrorMessage(error) || t("taskDocuments.failedToSave", "Failed to save document"), "error");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
async function handleCreateDocument() {
|
|
const key = newDocKey.trim();
|
|
const content = newDocContent.trim();
|
|
|
|
// Validate key
|
|
if (!key) {
|
|
addToast(t("taskDocuments.keyRequired", "Document key is required"), "error");
|
|
return;
|
|
}
|
|
if (!DOCUMENT_KEY_REGEX.test(key)) {
|
|
addToast(t("taskDocuments.invalidKeyFormat", "Invalid key format. Use 1-64 alphanumeric characters, hyphens, or underscores."), "error");
|
|
return;
|
|
}
|
|
if (!content) {
|
|
addToast(t("taskDocuments.contentRequired", "Content is required"), "error");
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
await putTaskDocument(taskId, key, content, {}, projectId);
|
|
setShowCreateForm(false);
|
|
setNewDocKey("");
|
|
setNewDocContent("");
|
|
await loadDocuments();
|
|
addToast(t("taskDocuments.created", "Document created"), "success");
|
|
} catch (error) {
|
|
addToast(getErrorMessage(error) || t("taskDocuments.failedToCreate", "Failed to create document"), "error");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
async function handleDeleteDocument(key: string) {
|
|
setDeletingKey(key);
|
|
try {
|
|
await deleteTaskDocument(taskId, key, projectId);
|
|
setConfirmDelete(null);
|
|
setDeletingKey(null);
|
|
if (expandedDocKey === key) {
|
|
setExpandedDocKey(null);
|
|
setExpandedContent("");
|
|
}
|
|
if (showHistory === key) {
|
|
setShowHistory(null);
|
|
setRevisions([]);
|
|
}
|
|
await loadDocuments();
|
|
addToast(t("taskDocuments.deleted", "Document deleted"), "success");
|
|
} catch (error) {
|
|
addToast(getErrorMessage(error) || t("taskDocuments.failedToDelete", "Failed to delete document"), "error");
|
|
} finally {
|
|
setDeletingKey(null);
|
|
}
|
|
}
|
|
|
|
function handleViewRevision(revision: TaskDocumentRevision) {
|
|
setExpandedContent(revision.content);
|
|
setEditingDocKey(null);
|
|
setEditContent("");
|
|
}
|
|
|
|
if (loading || artifactsLoading) {
|
|
return (
|
|
<div className="detail-section">
|
|
<h4>{t("taskDocuments.heading", "Artifacts")}</h4>
|
|
<div className="detail-log-empty"><LoadingSpinner label={t("taskDocuments.loading", "Loading documents and artifacts…")} /></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const isEmpty = documents.length === 0 && artifacts.length === 0 && !showCreateForm;
|
|
|
|
return (
|
|
<div className="detail-section">
|
|
<h4>{t("taskDocuments.heading", "Artifacts")}</h4>
|
|
|
|
{isEmpty && (
|
|
<div className="detail-log-empty">
|
|
{t("taskDocuments.noDocuments", "No documents or artifacts yet.")}
|
|
</div>
|
|
)}
|
|
|
|
{/*
|
|
* FNXC:ArtifactRegistry 2026-06-21-21:44:
|
|
* The per-task Artifacts tab must surface both traditional task documents and agent-created media artifacts so users can inspect all task-scoped outputs without leaving the task modal.
|
|
*/}
|
|
{artifacts.length > 0 && (
|
|
<section className="task-artifacts-section" aria-labelledby="task-artifacts-heading">
|
|
<div className="task-artifacts-section-header">
|
|
<h5 id="task-artifacts-heading">{t("taskDocuments.artifactsSubheading", "Media artifacts")}</h5>
|
|
<span className="task-artifacts-section-count">{t("taskDocuments.artifactCount", "{{count}} artifact{{plural}}", { count: artifacts.length, plural: artifacts.length === 1 ? "" : "s" })}</span>
|
|
</div>
|
|
<div className="documents-artifact-gallery documents-artifact-gallery--mobile task-artifacts-gallery">
|
|
{artifacts.map((artifact) => (
|
|
<TaskArtifactCard key={artifact.id} artifact={artifact} projectId={projectId} />
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<section className="task-documents-section" aria-labelledby="task-documents-heading">
|
|
<div className="task-artifacts-section-header">
|
|
<h5 id="task-documents-heading">{t("taskDocuments.documentsSubheading", "Task documents")}</h5>
|
|
{documents.length > 0 && (
|
|
<span className="task-artifacts-section-count">{t("taskDocuments.documentCount", "{{count}} document{{plural}}", { count: documents.length, plural: documents.length === 1 ? "" : "s" })}</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Create Form */}
|
|
{showCreateForm && (
|
|
<div className="task-document-create-form">
|
|
<h5>{t("taskDocuments.newDocumentTitle", "New Document")}</h5>
|
|
<div className="form-group">
|
|
<label htmlFor="doc-key">{t("taskDocuments.keyLabel", "Key")}</label>
|
|
<input
|
|
id="doc-key"
|
|
type="text"
|
|
className="task-document-key-input"
|
|
value={newDocKey}
|
|
onChange={(e) => setNewDocKey(e.target.value)}
|
|
placeholder={t("taskDocuments.keyPlaceholder", "e.g., plan, notes, research")}
|
|
disabled={saving}
|
|
/>
|
|
<span className="form-hint">{t("taskDocuments.keyHint", "Alphanumeric, hyphens, underscores (1-64 chars)")}</span>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="doc-content">{t("taskDocuments.contentLabel", "Content")}</label>
|
|
<textarea
|
|
id="doc-content"
|
|
value={newDocContent}
|
|
onChange={(e) => setNewDocContent(e.target.value)}
|
|
rows={6}
|
|
placeholder={t("taskDocuments.contentPlaceholder", "Enter document content…")}
|
|
disabled={saving}
|
|
/>
|
|
</div>
|
|
<div className="form-actions">
|
|
<button
|
|
className="btn btn-sm"
|
|
onClick={() => {
|
|
setShowCreateForm(false);
|
|
setNewDocKey("");
|
|
setNewDocContent("");
|
|
}}
|
|
disabled={saving}
|
|
>
|
|
{t("taskDocuments.cancel", "Cancel")}
|
|
</button>
|
|
<button
|
|
className="btn btn-primary btn-sm"
|
|
onClick={() => void handleCreateDocument()}
|
|
disabled={saving || !newDocKey.trim() || !newDocContent.trim()}
|
|
>
|
|
{saving ? t("taskDocuments.creating", "Creating…") : t("taskDocuments.create", "Create")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Document List */}
|
|
{documents.length === 0 && !showCreateForm ? (
|
|
!isEmpty && (
|
|
<div className="detail-log-empty">
|
|
{t("taskDocuments.noTaskDocuments", "No task documents yet.")}
|
|
</div>
|
|
)
|
|
) : (
|
|
<div className="task-documents-list">
|
|
{documents.map((doc) => (
|
|
<div key={doc.key} className="task-document-card">
|
|
<div className="task-document-card-header">
|
|
<div className="task-document-card-title">
|
|
<FileText size={14} />
|
|
<span className="task-document-key">{doc.key}</span>
|
|
<span className="task-document-revision-badge">v{doc.revision}</span>
|
|
</div>
|
|
<div className="task-document-meta">
|
|
<span className="task-document-author">{doc.author}</span>
|
|
<span className="task-document-timestamp">{formatTimestamp(doc.updatedAt || doc.createdAt)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Expanded Content View */}
|
|
{expandedDocKey === doc.key && editingDocKey !== doc.key && (
|
|
<>
|
|
<div className="task-document-content-header">
|
|
<button
|
|
className="btn btn-sm document-mode-toggle"
|
|
onClick={() => setRenderMarkdown((prev) => !prev)}
|
|
aria-label={renderMarkdown ? t("taskDocuments.switchToPlainText", "Switch to plain text") : t("taskDocuments.switchToMarkdown", "Switch to markdown")}
|
|
aria-pressed={renderMarkdown}
|
|
title={renderMarkdown ? t("taskDocuments.switchToPlainText", "Switch to plain text") : t("taskDocuments.switchToMarkdown", "Switch to markdown")}
|
|
>
|
|
{renderMarkdown ? t("taskDocuments.modeMarkdown", "Markdown") : t("taskDocuments.modePlain", "Plain")}
|
|
</button>
|
|
</div>
|
|
<div className="task-document-content">
|
|
{renderMarkdown ? (
|
|
<div className="task-document-content-markdown">
|
|
<div className="markdown-body">
|
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{expandedContent}</ReactMarkdown>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<pre className="task-document-content-text">{expandedContent}</pre>
|
|
)}
|
|
</div>
|
|
|
|
{/* Revision History */}
|
|
{showHistory === doc.key && (
|
|
<div className="task-document-revisions">
|
|
<h5>{t("taskDocuments.revisionHistory", "Revision History")}</h5>
|
|
{loadingRevisions ? (
|
|
<div className="detail-log-empty"><LoadingSpinner label={t("taskDocuments.loadingRevisions", "Loading…")} /></div>
|
|
) : revisions.length <= 1 ? (
|
|
<div className="detail-log-empty">{t("taskDocuments.noPreviousRevisions", "No previous revisions.")}</div>
|
|
) : (
|
|
<div className="task-document-revision-list">
|
|
{revisions
|
|
.filter((r) => r.revision < doc.revision)
|
|
.sort((a, b) => b.revision - a.revision)
|
|
.map((revision) => (
|
|
<div
|
|
key={revision.id}
|
|
className="task-document-revision-item"
|
|
onClick={() => handleViewRevision(revision)}
|
|
>
|
|
<div className="revision-header">
|
|
<span className="revision-badge">v{revision.revision}</span>
|
|
<span className="revision-author">{revision.author}</span>
|
|
<span className="revision-timestamp">{formatTimestamp(revision.createdAt)}</span>
|
|
</div>
|
|
<div className="revision-preview">{getContentPreview(revision.content, 100)}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Editing View */}
|
|
{editingDocKey === doc.key && (
|
|
<div className="task-document-edit-form">
|
|
<textarea
|
|
value={editContent}
|
|
onChange={(e) => setEditContent(e.target.value)}
|
|
rows={10}
|
|
disabled={saving}
|
|
/>
|
|
<div className="form-actions">
|
|
<button className="btn btn-sm" onClick={handleCancelEdit} disabled={saving}>
|
|
{t("taskDocuments.cancel", "Cancel")}
|
|
</button>
|
|
<button
|
|
className="btn btn-primary btn-sm"
|
|
onClick={() => void handleSaveEdit()}
|
|
disabled={saving || !editContent.trim()}
|
|
>
|
|
{saving ? t("taskDocuments.saving", "Saving…") : t("taskDocuments.save", "Save")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="task-document-actions">
|
|
<button
|
|
className="btn btn-sm"
|
|
onClick={() => void handleExpandDocument(doc)}
|
|
>
|
|
{expandedDocKey === doc.key ? (
|
|
<>
|
|
<ChevronUp size={14} /> {t("taskDocuments.collapse", "Collapse")}
|
|
</>
|
|
) : (
|
|
<>
|
|
<ChevronDown size={14} /> {t("taskDocuments.expand", "Expand")}
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
{expandedDocKey === doc.key && (
|
|
<>
|
|
<button
|
|
className="btn btn-sm"
|
|
onClick={() => void handleToggleHistory(doc.key)}
|
|
>
|
|
<History size={14} /> {t("taskDocuments.history", "History")}
|
|
</button>
|
|
|
|
{canEdit && editingDocKey !== doc.key && (
|
|
<button className="btn btn-sm" onClick={handleStartEdit}>
|
|
{t("taskDocuments.edit", "Edit")}
|
|
</button>
|
|
)}
|
|
|
|
{canEdit && (
|
|
confirmDelete === doc.key ? (
|
|
<div className="confirm-delete-actions">
|
|
<span>{t("taskDocuments.deleteConfirm", "Delete?")}</span>
|
|
<button
|
|
className="btn btn-danger btn-sm"
|
|
onClick={() => void handleDeleteDocument(doc.key)}
|
|
disabled={deletingKey === doc.key}
|
|
>
|
|
{deletingKey === doc.key ? "…" : t("taskDocuments.yes", "Yes")}
|
|
</button>
|
|
<button
|
|
className="btn btn-sm"
|
|
onClick={() => setConfirmDelete(null)}
|
|
disabled={deletingKey === doc.key}
|
|
>
|
|
{t("taskDocuments.no", "No")}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<button
|
|
className="btn btn-danger btn-sm"
|
|
onClick={() => setConfirmDelete(doc.key)}
|
|
>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
)
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* New Document Button */}
|
|
{canEdit && !showCreateForm && (
|
|
<button
|
|
className="btn btn-sm task-document-new-btn"
|
|
onClick={() => setShowCreateForm(true)}
|
|
>
|
|
<Plus size={14} /> {t("taskDocuments.newDocumentButton", "New Document")}
|
|
</button>
|
|
)}
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|