stuff
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { FolderIcon } from '../ui/icons';
|
||||
import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons';
|
||||
import DocumentThumbnailImage from './DocumentThumbnailImage';
|
||||
import CorrespondentLinks from './CorrespondentLinks';
|
||||
import { getTagColorStyle } from '../utils/colors';
|
||||
import { resolveCorrespondents } from './correspondents';
|
||||
import { writeTagTransferData } from './tagTransfer';
|
||||
import useInlineRename from './useInlineRename';
|
||||
|
||||
const DocumentsGrid = ({
|
||||
entries,
|
||||
@@ -35,7 +36,42 @@ const DocumentsGrid = ({
|
||||
onCorrespondentClick,
|
||||
activeCorrespondentIdSet,
|
||||
onClearSelection,
|
||||
}) => (
|
||||
onDocumentRename,
|
||||
onFolderRename,
|
||||
}) => {
|
||||
const {
|
||||
editingId: editingDocumentId,
|
||||
draftValue: documentDraft,
|
||||
setDraftValue: setDocumentDraft,
|
||||
beginEditing: beginDocumentEditing,
|
||||
cancelEditing: cancelDocumentEditing,
|
||||
submitEditing: submitDocumentEditing,
|
||||
savingId: savingDocumentId,
|
||||
attachInputRef: attachDocumentInputRef,
|
||||
} = useInlineRename(onDocumentRename, {
|
||||
getCurrentValue: (doc) => doc?.title ?? '',
|
||||
getEntityId: (doc) => doc?.id ?? null,
|
||||
});
|
||||
|
||||
const {
|
||||
editingId: editingFolderId,
|
||||
draftValue: folderDraft,
|
||||
setDraftValue: setFolderDraft,
|
||||
beginEditing: beginFolderEditing,
|
||||
cancelEditing: cancelFolderEditing,
|
||||
submitEditing: submitFolderEditing,
|
||||
savingId: savingFolderId,
|
||||
attachInputRef: attachFolderInputRef,
|
||||
} = useInlineRename(onFolderRename, {
|
||||
getCurrentValue: (folder) => folder?.name ?? '',
|
||||
getEntityId: (folder) => folder?.id ?? null,
|
||||
});
|
||||
|
||||
const documentSelectionCount = selectedDocumentIdsSet?.size ?? 0;
|
||||
const folderSelectionCount = selectedFolderIdsSet?.size ?? 0;
|
||||
const totalSelectionCount = documentSelectionCount + folderSelectionCount;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="documents-grid"
|
||||
role="list"
|
||||
@@ -58,6 +94,14 @@ const DocumentsGrid = ({
|
||||
const classes = ['document-card', 'folder-card'];
|
||||
if (isDraggingFolder) classes.push('is-dragging');
|
||||
if (isSelectedFolder) classes.push('selected');
|
||||
const canRenameFolder = folder.id !== 'root' && typeof onFolderRename === 'function';
|
||||
const isFolderEditing = editingFolderId === folder.id;
|
||||
const folderDraftValue = isFolderEditing ? folderDraft : folder.name;
|
||||
const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : '';
|
||||
const isFolderSaving = savingFolderId === folder.id;
|
||||
const canSubmitFolder =
|
||||
isFolderEditing && trimmedFolderDraft.length > 0 && trimmedFolderDraft !== folder.name;
|
||||
const allowInlineFolderEdit = canRenameFolder && isSelectedFolder && totalSelectionCount === 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -89,9 +133,83 @@ const DocumentsGrid = ({
|
||||
<FolderIcon className="folder-card__icon-svg" size={gridIconSize} />
|
||||
</div>
|
||||
<div className="folder-card__meta">
|
||||
<div className="folder-card__name" title={folder.name}>
|
||||
{folder.name}
|
||||
</div>
|
||||
{isFolderEditing ? (
|
||||
<div className="folder-card__edit doc-title-edit">
|
||||
<input
|
||||
type="text"
|
||||
ref={attachFolderInputRef}
|
||||
value={folderDraftValue}
|
||||
onChange={(event) => setFolderDraft(event.target.value)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
submitFolderEditing(folder);
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelFolderEditing(event);
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
const nextFocus = event.relatedTarget;
|
||||
if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
|
||||
cancelFolderEditing();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Save name"
|
||||
title="Save name"
|
||||
disabled={!canSubmitFolder || isFolderSaving}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
submitFolderEditing(folder);
|
||||
}}
|
||||
>
|
||||
<CheckIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Cancel"
|
||||
title="Cancel"
|
||||
onClick={(event) => cancelFolderEditing(event)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="folder-card__label-row">
|
||||
<span
|
||||
className="folder-card__name"
|
||||
title={folder.name}
|
||||
role={allowInlineFolderEdit ? 'button' : undefined}
|
||||
tabIndex={allowInlineFolderEdit ? 0 : undefined}
|
||||
onClick={(event) => {
|
||||
if (!allowInlineFolderEdit) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
beginFolderEditing(folder);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (!allowInlineFolderEdit) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
beginFolderEditing(folder);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{folder.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -111,6 +229,13 @@ const DocumentsGrid = ({
|
||||
const cardClasses = ['document-card', 'document'];
|
||||
if (isSelected) cardClasses.push('selected');
|
||||
if (isDraggingDoc) cardClasses.push('is-dragging');
|
||||
const isEditingDoc = editingDocumentId === doc.id;
|
||||
const documentDraftValue = isEditingDoc ? documentDraft : doc.title;
|
||||
const trimmedDocumentDraft = isEditingDoc ? documentDraft.trim() : '';
|
||||
const isDocumentSaving = savingDocumentId === doc.id;
|
||||
const canSubmitDocument =
|
||||
isEditingDoc && trimmedDocumentDraft.length > 0 && trimmedDocumentDraft !== doc.title;
|
||||
const allowInlineDocumentEdit = onDocumentRename && isSelected && totalSelectionCount === 1;
|
||||
return (
|
||||
<div
|
||||
key={entry.key}
|
||||
@@ -149,7 +274,82 @@ const DocumentsGrid = ({
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
<span className="doc-name__primary">{doc.title}</span>
|
||||
{isEditingDoc ? (
|
||||
<div className="document-card__title-edit doc-title-edit">
|
||||
<input
|
||||
type="text"
|
||||
ref={attachDocumentInputRef}
|
||||
value={documentDraftValue}
|
||||
onChange={(event) => setDocumentDraft(event.target.value)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
submitDocumentEditing(doc);
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelDocumentEditing(event);
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
const nextFocus = event.relatedTarget;
|
||||
if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
|
||||
cancelDocumentEditing();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Save title"
|
||||
title="Save title"
|
||||
disabled={!canSubmitDocument || isDocumentSaving}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
submitDocumentEditing(doc);
|
||||
}}
|
||||
>
|
||||
<CheckIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Cancel"
|
||||
title="Cancel"
|
||||
onClick={(event) => cancelDocumentEditing(event)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="document-card__title-row">
|
||||
<span
|
||||
className="document-card__title-badge"
|
||||
role={allowInlineDocumentEdit ? 'button' : undefined}
|
||||
tabIndex={allowInlineDocumentEdit ? 0 : undefined}
|
||||
onClick={(event) => {
|
||||
if (!allowInlineDocumentEdit) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
beginDocumentEditing(doc);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (!allowInlineDocumentEdit) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
beginDocumentEditing(doc);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{doc.title}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{visibleTags.length > 0 && (
|
||||
<div className="document-card__tags">
|
||||
@@ -204,6 +404,7 @@ const DocumentsGrid = ({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentsGrid;
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FolderIcon,
|
||||
EditIcon,
|
||||
DownloadIcon,
|
||||
TrashIcon,
|
||||
} from '../ui/icons';
|
||||
import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons';
|
||||
import { getTagColorStyle } from '../utils/colors';
|
||||
import DocumentThumbnailImage from './DocumentThumbnailImage';
|
||||
import CorrespondentLinks from './CorrespondentLinks';
|
||||
import { resolveCorrespondents } from './correspondents';
|
||||
import { writeTagTransferData } from './tagTransfer';
|
||||
import useInlineRename from './useInlineRename';
|
||||
|
||||
const formatDate = (value) => {
|
||||
if (!value) {
|
||||
return "—";
|
||||
}
|
||||
const timestamp = Date.parse(value);
|
||||
if (Number.isNaN(timestamp)) {
|
||||
return "—";
|
||||
}
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
};
|
||||
|
||||
const DocumentsList = ({
|
||||
entries,
|
||||
@@ -20,7 +27,6 @@ const DocumentsList = ({
|
||||
draggedFolderId,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
getDownloadHref,
|
||||
onFolderClick,
|
||||
onFolderSelect,
|
||||
onFolderDragOver,
|
||||
@@ -29,7 +35,6 @@ const DocumentsList = ({
|
||||
onFolderDragStart,
|
||||
onFolderDragEnd,
|
||||
onFolderRename,
|
||||
onFolderDelete,
|
||||
onDocumentClick,
|
||||
onDocumentOpen,
|
||||
onDocumentDragStart,
|
||||
@@ -38,20 +43,53 @@ const DocumentsList = ({
|
||||
onDocumentTagDragLeave,
|
||||
onDocumentTagDrop,
|
||||
onDocumentRename,
|
||||
onDocumentDelete,
|
||||
tagLookupById,
|
||||
onTagClick,
|
||||
onCorrespondentClick,
|
||||
activeCorrespondentIdSet,
|
||||
scrollRef,
|
||||
}) => (
|
||||
}) => {
|
||||
const {
|
||||
editingId: editingDocumentId,
|
||||
draftValue: documentDraft,
|
||||
setDraftValue: setDocumentDraft,
|
||||
beginEditing: beginDocumentEditing,
|
||||
cancelEditing: cancelDocumentEditing,
|
||||
submitEditing: submitDocumentEditing,
|
||||
savingId: savingDocumentId,
|
||||
attachInputRef: attachDocumentInputRef,
|
||||
} = useInlineRename(onDocumentRename, {
|
||||
getCurrentValue: (doc) => doc?.title ?? '',
|
||||
getEntityId: (doc) => doc?.id ?? null,
|
||||
});
|
||||
|
||||
const {
|
||||
editingId: editingFolderId,
|
||||
draftValue: folderDraft,
|
||||
setDraftValue: setFolderDraft,
|
||||
beginEditing: beginFolderEditing,
|
||||
cancelEditing: cancelFolderEditing,
|
||||
submitEditing: submitFolderEditing,
|
||||
savingId: savingFolderId,
|
||||
attachInputRef: attachFolderInputRef,
|
||||
} = useInlineRename(onFolderRename, {
|
||||
getCurrentValue: (folder) => folder?.name ?? '',
|
||||
getEntityId: (folder) => folder?.id ?? null,
|
||||
});
|
||||
|
||||
|
||||
const documentSelectionCount = selectedDocumentIdsSet?.size ?? 0;
|
||||
const folderSelectionCount = selectedFolderIdsSet?.size ?? 0;
|
||||
const totalSelectionCount = documentSelectionCount + folderSelectionCount;
|
||||
|
||||
return (
|
||||
<table aria-multiselectable="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="thumb-column"></th>
|
||||
<th>Name</th>
|
||||
<th>Issued</th>
|
||||
<th>Actions</th>
|
||||
<th>Added</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -65,6 +103,14 @@ const DocumentsList = ({
|
||||
const isDraggingFolder = draggedFolderId === folder.id;
|
||||
const isSelectedFolder = selectedFolderIdsSet?.has(folder.id);
|
||||
const rowKey = `folder:${folder.id}`;
|
||||
const canRenameFolder = folder.id !== 'root' && typeof onFolderRename === 'function';
|
||||
const isFolderEditing = editingFolderId === folder.id;
|
||||
const folderDraftValue = isFolderEditing ? folderDraft : folder.name;
|
||||
const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : '';
|
||||
const isFolderSaving = savingFolderId === folder.id;
|
||||
const canSubmitFolder =
|
||||
isFolderEditing && trimmedFolderDraft.length > 0 && trimmedFolderDraft !== folder.name;
|
||||
const allowInlineFolderEdit = canRenameFolder && isSelectedFolder && totalSelectionCount === 1;
|
||||
|
||||
return (
|
||||
<tr
|
||||
@@ -100,48 +146,90 @@ const DocumentsList = ({
|
||||
</td>
|
||||
<td className="doc-list__name">
|
||||
<div className="doc-list__name-content">
|
||||
<span>{folder.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
<td className="actions">
|
||||
<div className="action-buttons">
|
||||
{folder.id !== 'root' && onFolderRename && (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
title="Rename"
|
||||
aria-label={`Rename folder ${folder.name}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const nextName = window.prompt('Rename folder', folder.name);
|
||||
if (!nextName) {
|
||||
return;
|
||||
}
|
||||
const trimmed = nextName.trim();
|
||||
if (!trimmed || trimmed === folder.name) {
|
||||
return;
|
||||
}
|
||||
onFolderRename?.(folder.id, trimmed);
|
||||
}}
|
||||
>
|
||||
<EditIcon className="icon-inline" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button danger"
|
||||
title="Delete"
|
||||
aria-label={`Delete folder ${folder.name}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onFolderDelete?.(folder.id);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="icon-inline" />
|
||||
</button>
|
||||
<span className="doc-name__title">
|
||||
<span className="doc-name__primary">
|
||||
{isFolderEditing ? (
|
||||
<span className="doc-title-edit">
|
||||
<input
|
||||
type="text"
|
||||
ref={attachFolderInputRef}
|
||||
value={folderDraftValue}
|
||||
onChange={(event) => setFolderDraft(event.target.value)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
submitFolderEditing(folder);
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelFolderEditing(event);
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
const nextFocus = event.relatedTarget;
|
||||
if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
|
||||
cancelFolderEditing();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Save name"
|
||||
title="Save name"
|
||||
disabled={!canSubmitFolder || isFolderSaving}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
submitFolderEditing(folder);
|
||||
}}
|
||||
>
|
||||
<CheckIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Cancel"
|
||||
title="Cancel"
|
||||
onClick={(event) => {
|
||||
cancelFolderEditing(event);
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="doc-name__primary-text"
|
||||
role={allowInlineFolderEdit ? 'button' : undefined}
|
||||
tabIndex={allowInlineFolderEdit ? 0 : undefined}
|
||||
onClick={(event) => {
|
||||
if (!allowInlineFolderEdit) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
beginFolderEditing(folder);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (!allowInlineFolderEdit) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
beginFolderEditing(folder);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{folder.name}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>—</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -155,8 +243,17 @@ const DocumentsList = ({
|
||||
const rowClasses = ['document'];
|
||||
if (isSelected) rowClasses.push('selected');
|
||||
if (isDraggingDoc) rowClasses.push('is-dragging');
|
||||
const downloadHref = getDownloadHref?.(doc) || null;
|
||||
const correspondents = resolveCorrespondents(doc);
|
||||
const isEditingDoc = editingDocumentId === doc.id;
|
||||
const documentDraftValue = isEditingDoc ? documentDraft : doc.title;
|
||||
const trimmedDocumentDraft = isEditingDoc ? documentDraft.trim() : '';
|
||||
const isDocumentSaving = savingDocumentId === doc.id;
|
||||
const canSubmitDocument =
|
||||
isEditingDoc && trimmedDocumentDraft.length > 0 && trimmedDocumentDraft !== doc.title;
|
||||
const allowInlineDocumentEdit =
|
||||
onDocumentRename && isSelected && totalSelectionCount === 1;
|
||||
const issuedLabel = formatDate(doc.issued_at);
|
||||
const addedLabel = formatDate(doc.created_at || doc.uploaded_at);
|
||||
return (
|
||||
<tr
|
||||
key={entry.key}
|
||||
@@ -194,7 +291,84 @@ const DocumentsList = ({
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
<span className="doc-name__primary">{doc.title}</span>
|
||||
<span className="doc-name__primary">
|
||||
{isEditingDoc ? (
|
||||
<span className="doc-title-edit">
|
||||
<input
|
||||
type="text"
|
||||
ref={attachDocumentInputRef}
|
||||
value={documentDraftValue}
|
||||
onChange={(event) => setDocumentDraft(event.target.value)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
submitDocumentEditing(doc);
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelDocumentEditing(event);
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
const nextFocus = event.relatedTarget;
|
||||
if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
|
||||
cancelDocumentEditing();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Save title"
|
||||
title="Save title"
|
||||
disabled={!canSubmitDocument || isDocumentSaving}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
submitDocumentEditing(doc);
|
||||
}}
|
||||
>
|
||||
<CheckIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Cancel"
|
||||
title="Cancel"
|
||||
onClick={(event) => {
|
||||
cancelDocumentEditing(event);
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="doc-name__primary-text"
|
||||
role={allowInlineDocumentEdit ? 'button' : undefined}
|
||||
tabIndex={allowInlineDocumentEdit ? 0 : undefined}
|
||||
onClick={(event) => {
|
||||
if (!allowInlineDocumentEdit) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
beginDocumentEditing(doc);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (!allowInlineDocumentEdit) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
beginDocumentEditing(doc);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{doc.title}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{(doc.tags || []).length > 0 && (
|
||||
@@ -244,79 +418,14 @@ const DocumentsList = ({
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{(() => {
|
||||
const issuedAt = doc.issued_at || null;
|
||||
if (!issuedAt) {
|
||||
return '—';
|
||||
}
|
||||
const timestamp = Date.parse(issuedAt);
|
||||
if (Number.isNaN(timestamp)) {
|
||||
return '—';
|
||||
}
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
})()}
|
||||
</td>
|
||||
<td className="actions">
|
||||
<div className="action-buttons">
|
||||
{onDocumentRename && (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
title="Rename"
|
||||
aria-label={`Rename document ${doc.title}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const nextName = window.prompt('Rename document', doc.title);
|
||||
if (!nextName) {
|
||||
return;
|
||||
}
|
||||
const trimmed = nextName.trim();
|
||||
if (!trimmed || trimmed === doc.title) {
|
||||
return;
|
||||
}
|
||||
onDocumentRename?.(doc.id, trimmed);
|
||||
}}
|
||||
>
|
||||
<EditIcon className="icon-inline" />
|
||||
</button>
|
||||
)}
|
||||
{downloadHref ? (
|
||||
<a
|
||||
className="icon-button"
|
||||
href={downloadHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Download"
|
||||
aria-label="Download document"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onAuxClick={(event) => event.stopPropagation()}
|
||||
onContextMenu={(event) => event.stopPropagation()}
|
||||
>
|
||||
<DownloadIcon className="icon-inline" />
|
||||
</a>
|
||||
) : (
|
||||
<span className="meta">No download</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button danger"
|
||||
title="Delete"
|
||||
aria-label="Delete document"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onDocumentDelete?.(doc.id);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="icon-inline" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>{issuedLabel}</td>
|
||||
<td>{addedLabel}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentsList;
|
||||
|
||||
@@ -36,7 +36,6 @@ const DocumentsPanel = ({
|
||||
onFolderDragStart,
|
||||
onFolderDragEnd,
|
||||
draggedFolderId,
|
||||
onFolderDelete,
|
||||
onFolderRename,
|
||||
selectedFolderIds = [],
|
||||
onDocumentOpen,
|
||||
@@ -45,9 +44,8 @@ const DocumentsPanel = ({
|
||||
draggingDocumentIds = [],
|
||||
onDocumentDragStart,
|
||||
onDocumentDragEnd,
|
||||
onDocumentDelete,
|
||||
onDocumentRename,
|
||||
onRowSelection = null,
|
||||
onEntrySelection = null,
|
||||
onOpenDetailPanel = null,
|
||||
tagLookupById,
|
||||
activeCorrespondentIds = [],
|
||||
@@ -56,7 +54,6 @@ const DocumentsPanel = ({
|
||||
onFocusedRowChange,
|
||||
ensureAssetUrl = null,
|
||||
getDocumentAsset = () => null,
|
||||
getDownloadHref,
|
||||
onTagClick,
|
||||
onCorrespondentClick,
|
||||
isSearchLoading = false,
|
||||
@@ -253,8 +250,8 @@ const DocumentsPanel = ({
|
||||
|
||||
const rowKey = entry.type === EntryType.document ? `document:${entry.id}` : `folder:${entry.id}`;
|
||||
|
||||
if (rowKey && typeof onRowSelection === 'function') {
|
||||
onRowSelection(rowKey, event);
|
||||
if (rowKey && typeof onEntrySelection === 'function') {
|
||||
onEntrySelection(rowKey, event);
|
||||
}
|
||||
|
||||
if (entry.type === EntryType.document) {
|
||||
@@ -281,7 +278,7 @@ const DocumentsPanel = ({
|
||||
onFocusedRowChange?.(rowKey);
|
||||
}
|
||||
},
|
||||
[onRowSelection, onOpenDetailPanel, onFocusedRowChange, onFolderSelect],
|
||||
[onEntrySelection, onOpenDetailPanel, onFocusedRowChange, onFolderSelect],
|
||||
);
|
||||
|
||||
const handleDocumentClick = useCallback(
|
||||
@@ -472,6 +469,8 @@ const DocumentsPanel = ({
|
||||
onCorrespondentClick={onCorrespondentClick}
|
||||
activeCorrespondentIdSet={activeCorrespondentIdSet}
|
||||
onClearSelection={onClearSelection}
|
||||
onDocumentRename={onDocumentRename}
|
||||
onFolderRename={onFolderRename}
|
||||
/>
|
||||
) : !showTableRows ? null : (
|
||||
<DocumentsList
|
||||
@@ -483,7 +482,6 @@ const DocumentsPanel = ({
|
||||
draggedFolderId={draggedFolderId}
|
||||
ensureAssetUrl={ensureAssetUrl}
|
||||
getDocumentAsset={getDocumentAsset}
|
||||
getDownloadHref={getDownloadHref}
|
||||
onFolderClick={handleFolderClick}
|
||||
onFolderSelect={onFolderSelect}
|
||||
onFolderDragOver={onFolderDragOver}
|
||||
@@ -492,7 +490,6 @@ const DocumentsPanel = ({
|
||||
onFolderDragStart={onFolderDragStart}
|
||||
onFolderDragEnd={onFolderDragEnd}
|
||||
onFolderRename={onFolderRename}
|
||||
onFolderDelete={onFolderDelete}
|
||||
onDocumentClick={handleDocumentClick}
|
||||
onDocumentOpen={onDocumentOpen}
|
||||
onDocumentDragStart={handleDocumentDragStartLocal}
|
||||
@@ -501,7 +498,6 @@ const DocumentsPanel = ({
|
||||
onDocumentTagDragLeave={handleDocumentTagDragLeave}
|
||||
onDocumentTagDrop={handleDocumentTagDrop}
|
||||
onDocumentRename={onDocumentRename}
|
||||
onDocumentDelete={onDocumentDelete}
|
||||
tagLookupById={tagLookupById}
|
||||
onTagClick={onTagClick}
|
||||
onCorrespondentClick={onCorrespondentClick}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
const focusInput = (node) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const applyFocus = () => {
|
||||
node.focus();
|
||||
if (typeof node.select === 'function') {
|
||||
node.select();
|
||||
}
|
||||
};
|
||||
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
|
||||
window.requestAnimationFrame(applyFocus);
|
||||
} else {
|
||||
applyFocus();
|
||||
}
|
||||
};
|
||||
|
||||
const identity = (value) => value;
|
||||
|
||||
const useInlineRename = (
|
||||
onRename,
|
||||
{
|
||||
getCurrentValue = identity,
|
||||
getEntityId = (entity) => entity?.id ?? null,
|
||||
} = {},
|
||||
) => {
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [draftValue, setDraftValue] = useState('');
|
||||
const [savingId, setSavingId] = useState(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setEditingId(null);
|
||||
setDraftValue('');
|
||||
setSavingId(null);
|
||||
inputRef.current = null;
|
||||
}, []);
|
||||
|
||||
const beginEditing = useCallback(
|
||||
(entity, event) => {
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
const entityId = getEntityId(entity);
|
||||
if (!entityId) {
|
||||
return;
|
||||
}
|
||||
const currentValue = getCurrentValue(entity) ?? '';
|
||||
setEditingId(entityId);
|
||||
setDraftValue(currentValue);
|
||||
setSavingId(null);
|
||||
},
|
||||
[getCurrentValue, getEntityId],
|
||||
);
|
||||
|
||||
const cancelEditing = useCallback(
|
||||
(event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
resetState();
|
||||
},
|
||||
[resetState],
|
||||
);
|
||||
|
||||
const submitEditing = useCallback(
|
||||
async (entity) => {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
const entityId = getEntityId(entity);
|
||||
if (!entityId || editingId !== entityId) {
|
||||
return false;
|
||||
}
|
||||
const trimmed = draftValue.trim();
|
||||
const currentValue = getCurrentValue(entity) ?? '';
|
||||
if (!trimmed || trimmed === currentValue) {
|
||||
resetState();
|
||||
return true;
|
||||
}
|
||||
if (typeof onRename !== 'function') {
|
||||
resetState();
|
||||
return true;
|
||||
}
|
||||
setSavingId(entityId);
|
||||
try {
|
||||
const result = await onRename(entityId, trimmed);
|
||||
if (result === false) {
|
||||
return false;
|
||||
}
|
||||
resetState();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
} finally {
|
||||
setSavingId((current) => (current === entityId ? null : current));
|
||||
}
|
||||
},
|
||||
[draftValue, editingId, getCurrentValue, getEntityId, onRename, resetState],
|
||||
);
|
||||
|
||||
const attachInputRef = useCallback(
|
||||
(node) => {
|
||||
if (node) {
|
||||
inputRef.current = node;
|
||||
focusInput(node);
|
||||
} else if (inputRef.current) {
|
||||
inputRef.current = null;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
editingId,
|
||||
draftValue,
|
||||
setDraftValue,
|
||||
beginEditing,
|
||||
cancelEditing,
|
||||
submitEditing,
|
||||
savingId,
|
||||
attachInputRef,
|
||||
};
|
||||
};
|
||||
|
||||
export default useInlineRename;
|
||||
Reference in New Issue
Block a user