refactor: Decompose document and folder views into modular components and hooks, replacing monolithic list/grid implementations.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
|
||||
import type { DocumentViewLogic } from '../hooks/useDocumentViewLogic';
|
||||
import { useDocumentItemLogic } from '../hooks/useDocumentItemLogic';
|
||||
import EntryShell from './EntryShell';
|
||||
|
||||
interface DocumentEntryProps extends DocumentsViewProps {
|
||||
doc: any;
|
||||
viewLogic: DocumentViewLogic;
|
||||
component: React.ElementType;
|
||||
className?: string;
|
||||
role?: string;
|
||||
children: (logic: ReturnType<typeof useDocumentItemLogic>) => React.ReactNode;
|
||||
}
|
||||
|
||||
const DocumentEntry: React.FC<DocumentEntryProps> = (props) => {
|
||||
const { doc, component, className, role, children, viewLogic } = props;
|
||||
const logic = useDocumentItemLogic({ doc, viewLogic, ...props });
|
||||
|
||||
return (
|
||||
<EntryShell
|
||||
component={component}
|
||||
id={`document-${doc.id}`}
|
||||
docId={doc.id}
|
||||
handlers={logic.handlers}
|
||||
isSelected={logic.isSelected}
|
||||
isDragging={logic.isDraggingDoc}
|
||||
canDrag={true}
|
||||
className={className}
|
||||
role={role}
|
||||
>
|
||||
{children(logic)}
|
||||
</EntryShell>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentEntry;
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { getTagColorStyle } from '../../utils/colors';
|
||||
import { writeTagTransferData } from '../tagTransfer';
|
||||
import type { DocumentTag } from '../../types/documents';
|
||||
import type { Identifier } from '../../types/identifiers';
|
||||
|
||||
interface DocumentTagsProps {
|
||||
tags: DocumentTag[];
|
||||
tagLookupById?: Map<Identifier, DocumentTag> | null;
|
||||
onTagClick?: (tagId: Identifier) => void;
|
||||
docId: Identifier;
|
||||
maxTags?: number;
|
||||
}
|
||||
|
||||
const DocumentTags: React.FC<DocumentTagsProps> = ({
|
||||
tags,
|
||||
tagLookupById,
|
||||
onTagClick,
|
||||
docId,
|
||||
maxTags,
|
||||
}) => {
|
||||
const visibleTags = maxTags ? tags.slice(0, maxTags) : tags;
|
||||
const remainingTagCount = maxTags && tags.length > maxTags ? tags.length - maxTags : 0;
|
||||
|
||||
if (tags.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{visibleTags.map((tag, index) => {
|
||||
const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color;
|
||||
const style = getTagColorStyle(colorSource);
|
||||
const tagId = tag?.id ?? null;
|
||||
const clickable = tagId != null && typeof onTagClick === 'function';
|
||||
const key = tagId ?? `${docId}-tag-${index}`;
|
||||
|
||||
return (
|
||||
<span
|
||||
key={key}
|
||||
className="badge tag-chip"
|
||||
style={style || undefined}
|
||||
title={tag.label}
|
||||
role={clickable ? 'button' : undefined}
|
||||
onClick={clickable ? (event) => {
|
||||
event.stopPropagation();
|
||||
if (tagId == null) return;
|
||||
onTagClick?.(tagId);
|
||||
} : undefined}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'copyMove';
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[documents] Failed to configure drag effect', error);
|
||||
}
|
||||
writeTagTransferData(event.dataTransfer, tag, docId);
|
||||
}}
|
||||
onDragEnd={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onKeyDown={clickable ? (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (tagId == null) return;
|
||||
onTagClick?.(tagId);
|
||||
}
|
||||
} : undefined}
|
||||
>
|
||||
{tag.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{remainingTagCount > 0 && (
|
||||
<span className="badge tag-chip tag-chip--more">+{remainingTagCount}</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentTags;
|
||||
@@ -0,0 +1,129 @@
|
||||
import React from 'react';
|
||||
import { FolderIcon } from '../../ui/icons';
|
||||
import DocumentThumbnailImage from '../DocumentThumbnailImage';
|
||||
import { resolveCorrespondents } from '../correspondents';
|
||||
import type { DocumentsListEntry } from '../../types/documents';
|
||||
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
|
||||
import type { DocumentViewLogic } from '../hooks/useDocumentViewLogic';
|
||||
import EditableEntryTitle from './EditableEntryTitle';
|
||||
import EntryCorrespondents from './EntryCorrespondents';
|
||||
import EntryTags from './EntryTags';
|
||||
import FolderEntry from './FolderEntry';
|
||||
import DocumentEntry from './DocumentEntry';
|
||||
|
||||
interface DocumentsGridCardProps extends DocumentsViewProps {
|
||||
entry: DocumentsListEntry;
|
||||
viewLogic: DocumentViewLogic;
|
||||
gridIconSize?: number;
|
||||
}
|
||||
|
||||
const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
|
||||
const { entry, gridIconSize } = props;
|
||||
|
||||
if (entry.type === 'folder') {
|
||||
const folder = entry.folder;
|
||||
if (!folder) return null;
|
||||
|
||||
return (
|
||||
<FolderEntry
|
||||
{...props}
|
||||
folder={folder}
|
||||
component="div"
|
||||
className="document-card folder-card"
|
||||
role="listitem"
|
||||
>
|
||||
{(logic) => (
|
||||
<>
|
||||
<div className="folder-card__icon">
|
||||
<FolderIcon className="folder-card__icon-svg" size={gridIconSize} />
|
||||
</div>
|
||||
<div className="folder-card__meta">
|
||||
<div className="folder-card__label-row">
|
||||
<EditableEntryTitle
|
||||
isEditing={logic.isFolderEditing}
|
||||
draftValue={logic.folderDraftValue}
|
||||
onChange={logic.handlers.onRenameChange}
|
||||
onSubmit={logic.handlers.onRenameSubmit}
|
||||
onCancel={logic.handlers.onRenameCancel}
|
||||
isSaving={logic.isFolderSaving}
|
||||
canSubmit={logic.canSubmitFolder}
|
||||
inputRef={logic.attachFolderInputRef}
|
||||
allowInlineEdit={logic.allowInlineFolderEdit}
|
||||
onBeginEditing={logic.handlers.onRenameBegin}
|
||||
className="folder-card__name"
|
||||
>
|
||||
{folder.name}
|
||||
</EditableEntryTitle>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</FolderEntry>
|
||||
);
|
||||
}
|
||||
|
||||
const doc = entry.document;
|
||||
if (!doc) return null;
|
||||
|
||||
const correspondents = resolveCorrespondents(doc);
|
||||
|
||||
return (
|
||||
<DocumentEntry
|
||||
{...props}
|
||||
doc={doc}
|
||||
component="div"
|
||||
className="document-card document"
|
||||
role="listitem"
|
||||
>
|
||||
{(logic) => (
|
||||
<>
|
||||
<DocumentThumbnailImage
|
||||
document={doc}
|
||||
ensureAssetUrl={props.ensureAssetUrl}
|
||||
getDocumentAsset={props.getDocumentAsset}
|
||||
alt={`Thumbnail for ${doc.title}`}
|
||||
maxSize={gridIconSize}
|
||||
scrollRootRef={props.scrollRef}
|
||||
/>
|
||||
<div className="document-card__meta">
|
||||
<div className="document-card__title" title={doc.title}>
|
||||
<EntryCorrespondents
|
||||
correspondents={correspondents}
|
||||
activeCorrespondentIdSet={props.activeCorrespondentIdSet}
|
||||
onCorrespondentClick={props.onCorrespondentClick}
|
||||
/>
|
||||
<div className="document-card__title-row">
|
||||
<EditableEntryTitle
|
||||
isEditing={logic.isEditingDoc}
|
||||
draftValue={logic.documentDraftValue}
|
||||
onChange={logic.handlers.onRenameChange}
|
||||
onSubmit={logic.handlers.onRenameSubmit}
|
||||
onCancel={logic.handlers.onRenameCancel}
|
||||
isSaving={logic.isDocumentSaving}
|
||||
canSubmit={logic.canSubmitDocument}
|
||||
inputRef={logic.attachDocumentInputRef}
|
||||
allowInlineEdit={logic.allowInlineDocumentEdit}
|
||||
onBeginEditing={logic.handlers.onRenameBegin}
|
||||
className="document-card__title-badge"
|
||||
>
|
||||
{doc.title}
|
||||
</EditableEntryTitle>
|
||||
</div>
|
||||
</div>
|
||||
<div className="document-card__tags">
|
||||
<EntryTags
|
||||
tags={doc.tags || []}
|
||||
tagLookupById={props.tagLookupById}
|
||||
onTagClick={props.onTagClick}
|
||||
docId={doc.id}
|
||||
maxTags={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DocumentEntry>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentsGridCard;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
interface DocumentsGridContainerProps {
|
||||
children: React.ReactNode;
|
||||
clearSelection: () => void;
|
||||
gridIconSize?: number;
|
||||
}
|
||||
|
||||
const DocumentsGridContainer: React.FC<DocumentsGridContainerProps> = ({
|
||||
children,
|
||||
clearSelection,
|
||||
gridIconSize,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="documents-grid"
|
||||
role="list"
|
||||
style={
|
||||
gridIconSize
|
||||
? ({ '--documents-grid-icon-size': `${gridIconSize}px` } as React.CSSProperties)
|
||||
: undefined
|
||||
}
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
clearSelection();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentsGridContainer;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
interface DocumentsListContainerProps {
|
||||
children: React.ReactNode;
|
||||
clearSelection: () => void;
|
||||
}
|
||||
|
||||
const DocumentsListContainer: React.FC<DocumentsListContainerProps> = ({
|
||||
children,
|
||||
clearSelection,
|
||||
}) => {
|
||||
return (
|
||||
<table aria-multiselectable="true">
|
||||
<thead
|
||||
onClick={() => {
|
||||
clearSelection();
|
||||
}}
|
||||
>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>Name</th>
|
||||
<th>Issued</th>
|
||||
<th>Added</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{children}</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentsListContainer;
|
||||
@@ -0,0 +1,143 @@
|
||||
import React from 'react';
|
||||
import { FolderIcon } from '../../ui/icons';
|
||||
import { formatDate } from '../../utils/date';
|
||||
import DocumentThumbnailImage from '../DocumentThumbnailImage';
|
||||
import { resolveCorrespondents } from '../correspondents';
|
||||
import type { DocumentsListEntry } from '../../types/documents';
|
||||
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
|
||||
import type { DocumentViewLogic } from '../hooks/useDocumentViewLogic';
|
||||
import EditableEntryTitle from './EditableEntryTitle';
|
||||
import EntryCorrespondents from './EntryCorrespondents';
|
||||
import EntryTags from './EntryTags';
|
||||
import FolderEntry from './FolderEntry';
|
||||
import DocumentEntry from './DocumentEntry';
|
||||
|
||||
interface DocumentsListRowProps extends DocumentsViewProps {
|
||||
entry: DocumentsListEntry;
|
||||
viewLogic: DocumentViewLogic;
|
||||
}
|
||||
|
||||
const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
|
||||
const { entry } = props;
|
||||
|
||||
if (entry.type === 'folder') {
|
||||
const folder = entry.folder;
|
||||
if (!folder) return null;
|
||||
|
||||
return (
|
||||
<FolderEntry
|
||||
{...props}
|
||||
folder={folder}
|
||||
component="tr"
|
||||
className="folder"
|
||||
>
|
||||
{(logic) => (
|
||||
<>
|
||||
<td className="thumb-cell">
|
||||
<div className="thumb-icon">
|
||||
<FolderIcon className="thumb-icon__image" size={32} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="doc-list__name">
|
||||
<div className="doc-list__name-content">
|
||||
<span className="doc-name__title">
|
||||
<span className="doc-name__primary">
|
||||
<EditableEntryTitle
|
||||
isEditing={logic.isFolderEditing}
|
||||
draftValue={logic.folderDraftValue}
|
||||
onChange={logic.handlers.onRenameChange}
|
||||
onSubmit={logic.handlers.onRenameSubmit}
|
||||
onCancel={logic.handlers.onRenameCancel}
|
||||
isSaving={logic.isFolderSaving}
|
||||
canSubmit={logic.canSubmitFolder}
|
||||
inputRef={logic.attachFolderInputRef}
|
||||
allowInlineEdit={logic.allowInlineFolderEdit}
|
||||
onBeginEditing={logic.handlers.onRenameBegin}
|
||||
className="doc-name__primary-text"
|
||||
>
|
||||
{folder.name}
|
||||
</EditableEntryTitle>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>—</td>
|
||||
<td>—</td>
|
||||
</>
|
||||
)}
|
||||
</FolderEntry>
|
||||
);
|
||||
}
|
||||
|
||||
const doc = entry.document;
|
||||
if (!doc) return null;
|
||||
|
||||
const correspondents = resolveCorrespondents(doc);
|
||||
const issuedLabel = formatDate(doc.issued_at);
|
||||
const addedLabel = formatDate(doc.created_at || doc.uploaded_at);
|
||||
|
||||
return (
|
||||
<DocumentEntry
|
||||
{...props}
|
||||
doc={doc}
|
||||
component="tr"
|
||||
className="document"
|
||||
>
|
||||
{(logic) => (
|
||||
<>
|
||||
<td className="thumb-cell">
|
||||
<DocumentThumbnailImage
|
||||
document={doc}
|
||||
ensureAssetUrl={props.ensureAssetUrl}
|
||||
getDocumentAsset={props.getDocumentAsset}
|
||||
alt={`Thumbnail for ${doc.title}`}
|
||||
scrollRootRef={props.scrollRef}
|
||||
/>
|
||||
</td>
|
||||
<td className="doc-list__name">
|
||||
<div className="doc-name">
|
||||
<div className="doc-list__name-content">
|
||||
<span className="doc-name__title">
|
||||
<EntryCorrespondents
|
||||
correspondents={correspondents}
|
||||
activeCorrespondentIdSet={props.activeCorrespondentIdSet}
|
||||
onCorrespondentClick={props.onCorrespondentClick}
|
||||
/>
|
||||
<span className="doc-name__primary">
|
||||
<EditableEntryTitle
|
||||
isEditing={logic.isEditingDoc}
|
||||
draftValue={logic.documentDraftValue}
|
||||
onChange={logic.handlers.onRenameChange}
|
||||
onSubmit={logic.handlers.onRenameSubmit}
|
||||
onCancel={logic.handlers.onRenameCancel}
|
||||
isSaving={logic.isDocumentSaving}
|
||||
canSubmit={logic.canSubmitDocument}
|
||||
inputRef={logic.attachDocumentInputRef}
|
||||
allowInlineEdit={logic.allowInlineDocumentEdit}
|
||||
onBeginEditing={logic.handlers.onRenameBegin}
|
||||
className="doc-name__primary-text"
|
||||
>
|
||||
{doc.title}
|
||||
</EditableEntryTitle>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="doc-name__tags">
|
||||
<EntryTags
|
||||
tags={doc.tags || []}
|
||||
tagLookupById={props.tagLookupById}
|
||||
onTagClick={props.onTagClick}
|
||||
docId={doc.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{issuedLabel}</td>
|
||||
<td>{addedLabel}</td>
|
||||
</>
|
||||
)}
|
||||
</DocumentEntry>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentsListRow;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import InlineRenameInput from './InlineRenameInput';
|
||||
|
||||
interface EditableEntryTitleProps {
|
||||
isEditing: boolean;
|
||||
draftValue: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
onCancel: (event?: React.SyntheticEvent) => void;
|
||||
isSaving: boolean;
|
||||
canSubmit: boolean;
|
||||
inputRef: (ref: HTMLInputElement | null) => void;
|
||||
allowInlineEdit: boolean | undefined;
|
||||
onBeginEditing: (event: React.SyntheticEvent) => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const EditableEntryTitle: React.FC<EditableEntryTitleProps> = ({
|
||||
isEditing,
|
||||
draftValue,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
isSaving,
|
||||
canSubmit,
|
||||
inputRef,
|
||||
allowInlineEdit,
|
||||
onBeginEditing,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className={`doc-title-edit ${className || ''}`}>
|
||||
<InlineRenameInput
|
||||
value={draftValue}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
isSaving={isSaving}
|
||||
canSubmit={canSubmit}
|
||||
inputRef={inputRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={className}
|
||||
role={allowInlineEdit ? 'button' : undefined}
|
||||
tabIndex={allowInlineEdit ? 0 : undefined}
|
||||
onClick={onBeginEditing}
|
||||
onKeyDown={(event) => {
|
||||
if (!allowInlineEdit) return;
|
||||
if (event.key === 'Enter') {
|
||||
onBeginEditing(event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditableEntryTitle;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import CorrespondentLinks from '../CorrespondentLinks';
|
||||
import type { Identifier } from '../../types/identifiers';
|
||||
|
||||
interface EntryCorrespondentsProps {
|
||||
correspondents: any[];
|
||||
activeCorrespondentIdSet?: Set<Identifier> | null;
|
||||
onCorrespondentClick?: (correspondentId: Identifier) => void;
|
||||
}
|
||||
|
||||
const EntryCorrespondents: React.FC<EntryCorrespondentsProps> = (props) => {
|
||||
if (!props.correspondents || props.correspondents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="doc-correspondents">
|
||||
<CorrespondentLinks
|
||||
correspondents={props.correspondents}
|
||||
activeCorrespondentIdSet={props.activeCorrespondentIdSet}
|
||||
onCorrespondentClick={props.onCorrespondentClick}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntryCorrespondents;
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { type DragEvent } from 'react';
|
||||
|
||||
export interface EntryShellHandlers {
|
||||
onClick: (event: React.MouseEvent) => void;
|
||||
onDoubleClick: (event: React.MouseEvent) => void;
|
||||
onDragStart: (event: DragEvent<HTMLElement>) => void;
|
||||
onDragEnd: (event: DragEvent<HTMLElement>) => void;
|
||||
onDragOver: (event: DragEvent<HTMLElement>) => void;
|
||||
onDragLeave: (event: DragEvent<HTMLElement>) => void;
|
||||
onDrop: (event: DragEvent<HTMLElement>) => void;
|
||||
onDragOverCapture?: (event: DragEvent<HTMLElement>) => void;
|
||||
onDragLeaveCapture?: (event: DragEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
interface EntryShellProps {
|
||||
component: React.ElementType;
|
||||
handlers: EntryShellHandlers;
|
||||
isSelected?: boolean;
|
||||
isDragging?: boolean;
|
||||
canDrag?: boolean;
|
||||
id: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
docId?: number;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
const EntryShell: React.FC<EntryShellProps> = ({
|
||||
component: Component,
|
||||
handlers,
|
||||
isSelected,
|
||||
isDragging,
|
||||
canDrag,
|
||||
id,
|
||||
className = '',
|
||||
children,
|
||||
docId,
|
||||
role,
|
||||
}) => {
|
||||
const classes = [className];
|
||||
if (isSelected) classes.push('selected');
|
||||
if (isDragging) classes.push('is-dragging');
|
||||
|
||||
const commonProps = {
|
||||
id,
|
||||
className: classes.join(' '),
|
||||
onClick: handlers.onClick,
|
||||
onDoubleClick: handlers.onDoubleClick,
|
||||
draggable: canDrag,
|
||||
onDragStart: handlers.onDragStart,
|
||||
onDragEnd: handlers.onDragEnd,
|
||||
onDragOver: handlers.onDragOver,
|
||||
onDragLeave: handlers.onDragLeave,
|
||||
onDrop: handlers.onDrop,
|
||||
...(docId ? { 'data-doc-id': docId } : {}),
|
||||
...(role ? { role } : {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<Component {...commonProps}>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntryShell;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import DocumentTags from './DocumentTags';
|
||||
import type { Identifier } from '../../types/identifiers';
|
||||
import type { DocumentTag } from '../../types/documents';
|
||||
|
||||
interface EntryTagsProps {
|
||||
tags: Identifier[];
|
||||
tagLookupById?: Map<Identifier, DocumentTag> | null;
|
||||
onTagClick?: (tagId: Identifier) => void;
|
||||
docId: Identifier;
|
||||
maxTags?: number;
|
||||
}
|
||||
|
||||
const EntryTags: React.FC<EntryTagsProps> = (props) => {
|
||||
if (!props.tags || props.tags.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DocumentTags
|
||||
tags={props.tags}
|
||||
tagLookupById={props.tagLookupById}
|
||||
onTagClick={props.onTagClick}
|
||||
docId={props.docId}
|
||||
maxTags={props.maxTags}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntryTags;
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import type { DocumentsViewProps } from '../panel/DocumentsPanel';
|
||||
import type { DocumentViewLogic } from '../hooks/useDocumentViewLogic';
|
||||
import { useFolderItemLogic } from '../hooks/useFolderItemLogic';
|
||||
import EntryShell from './EntryShell';
|
||||
|
||||
interface FolderEntryProps extends DocumentsViewProps {
|
||||
folder: any;
|
||||
viewLogic: DocumentViewLogic;
|
||||
component: React.ElementType;
|
||||
className?: string;
|
||||
role?: string;
|
||||
children: (logic: ReturnType<typeof useFolderItemLogic>) => React.ReactNode;
|
||||
}
|
||||
|
||||
const FolderEntry: React.FC<FolderEntryProps> = (props) => {
|
||||
const { folder, component, className, role, children, viewLogic } = props;
|
||||
const logic = useFolderItemLogic({ folder, viewLogic, ...props });
|
||||
|
||||
return (
|
||||
<EntryShell
|
||||
component={component}
|
||||
id={`folder-${folder.id}`}
|
||||
handlers={logic.handlers}
|
||||
isSelected={logic.isSelectedFolder}
|
||||
isDragging={logic.isDraggingFolder}
|
||||
canDrag={logic.canDragFolder}
|
||||
className={className}
|
||||
role={role}
|
||||
>
|
||||
{children(logic)}
|
||||
</EntryShell>
|
||||
);
|
||||
};
|
||||
|
||||
export default FolderEntry;
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { CheckIcon, CloseIcon } from '../../ui/icons';
|
||||
|
||||
interface InlineRenameInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
onCancel: (event?: React.SyntheticEvent) => void;
|
||||
isSaving?: boolean;
|
||||
canSubmit?: boolean;
|
||||
inputRef?: React.Ref<HTMLInputElement>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const InlineRenameInput: React.FC<InlineRenameInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
isSaving = false,
|
||||
canSubmit = true,
|
||||
inputRef,
|
||||
className = 'doc-title-edit',
|
||||
}) => {
|
||||
return (
|
||||
<span className={className}>
|
||||
<input
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
onCancel(event);
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
const nextFocus = event.relatedTarget;
|
||||
if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
|
||||
onCancel();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Save"
|
||||
title="Save"
|
||||
disabled={!canSubmit || isSaving}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<CheckIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Cancel"
|
||||
title="Cancel"
|
||||
onClick={(event) => {
|
||||
onCancel(event);
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default InlineRenameInput;
|
||||
Reference in New Issue
Block a user