fuck
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import DocumentSummarySection, { DocumentSummarySectionProps } from './DocumentSummarySection';
|
||||
import { buildDocumentMetadataItems, extractDocumentMetadataPayload } from './documentMetadata';
|
||||
import { describeDocumentSummary, extractDocumentMetadataPayload, type DocumentSummaryRow } from './documentSummary';
|
||||
|
||||
type PanelTab = { id: string; label: string; render: (context?: Record<string, unknown>) => ReactNode };
|
||||
|
||||
@@ -15,8 +15,8 @@ type ContentState =
|
||||
|
||||
export interface DocumentInfoPanelProps {
|
||||
document: DocumentSummarySectionProps['document'];
|
||||
summaryProps?: Omit<DocumentSummarySectionProps, 'document' | 'detailItems' | 'layout'>;
|
||||
metadataItems?: Array<{ label: string; value?: string }>;
|
||||
summaryProps?: Omit<DocumentSummarySectionProps, 'document' | 'layout'>;
|
||||
metadataItems?: DocumentSummaryRow[];
|
||||
metadataPayload?: Record<string, unknown>;
|
||||
metadataTabLabel?: string;
|
||||
detailsTabLabel?: string;
|
||||
@@ -76,7 +76,7 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
|
||||
if (Array.isArray(metadataItemsProp) && metadataItemsProp.length) {
|
||||
return metadataItemsProp;
|
||||
}
|
||||
return buildDocumentMetadataItems(document);
|
||||
return describeDocumentSummary(document);
|
||||
}, [metadataItemsProp, document]);
|
||||
|
||||
const metadataPayload = useMemo(() => {
|
||||
@@ -149,18 +149,17 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
|
||||
const renderSummarySection = useCallback(() => (
|
||||
<DocumentSummarySection
|
||||
document={document}
|
||||
detailItems={metadataItems}
|
||||
layout={summaryLayout}
|
||||
{...summaryProps}
|
||||
/>
|
||||
), [document, summaryLayout, summaryProps, metadataItems]);
|
||||
), [document, summaryLayout, summaryProps]);
|
||||
|
||||
const renderDetailsSection = useCallback(() => (
|
||||
<section className={`${base}__section`}>
|
||||
{metadataItems.length ? (
|
||||
<dl className={`${base}__section-list`}>
|
||||
{metadataItems.map(({ label, value }) => (
|
||||
<div className={`${base}__section-item`} key={label}>
|
||||
{metadataItems.map(({ key, label, value }) => (
|
||||
<div className={`${base}__section-item`} key={key || label}>
|
||||
<dt>{label}</dt>
|
||||
<dd>{value || '—'}</dd>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { EditIcon, IconX, PlusIcon } from '../ui/icons';
|
||||
import SelectionAssignmentMenu, { SelectionAssignmentMenuItem } from './SelectionAssignmentMenu';
|
||||
import React, { useCallback, useEffect, useMemo, useState, type ReactNode, type FormEvent } from 'react';
|
||||
import { EditIcon, IconX, CheckIcon, PlusIcon } from '../ui/icons';
|
||||
import SelectionAssignmentMenu, {
|
||||
SelectionAssignmentMenuItem,
|
||||
type NormalizedSelectionAssignmentItem,
|
||||
} from './SelectionAssignmentMenu';
|
||||
import { getTagColorStyle } from '../utils/colors';
|
||||
import {
|
||||
formatDate,
|
||||
toDateInputValue,
|
||||
toIssuedTimestamp,
|
||||
} from '../utils/date';
|
||||
import { describeDocumentSummary } from './documentSummary';
|
||||
import { describeDocumentSummary, type DocumentSummaryRow } from './documentSummary';
|
||||
import { isPlainObject } from '../utils/typeGuards';
|
||||
|
||||
type Identifier = string | number;
|
||||
@@ -70,7 +72,14 @@ export interface DocumentSummarySectionProps {
|
||||
onUpdateTitle?: (docId: Identifier | undefined, title: string) => Promise<boolean> | boolean;
|
||||
onUpdateIssued?: (docId: Identifier | undefined, timestamp: number | null) => Promise<boolean> | boolean;
|
||||
layout?: 'default' | 'compact';
|
||||
detailItems?: Array<{ label?: string; value?: string }>;
|
||||
}
|
||||
|
||||
interface MetaItem {
|
||||
key: string;
|
||||
label: string;
|
||||
valueContent?: React.ReactNode | null;
|
||||
fallbackValue?: string | null;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export const sortCorrespondents = (entries = []) =>
|
||||
@@ -122,7 +131,7 @@ const resolveOptionName = (source?: QuickAddOption | string | null): string => {
|
||||
return `${source}`.trim();
|
||||
};
|
||||
|
||||
const normalizeQuickAddOption = (option: QuickAddOption | string | null | undefined): QuickAddEntry | null => {
|
||||
const normalizeQuickAddOption = (option?: QuickAddOption | string | null): QuickAddEntry | null => {
|
||||
if (option == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -227,7 +236,7 @@ export const TagSection: React.FC<TagSectionProps> = ({
|
||||
}, [normalizedOptions, tags]);
|
||||
|
||||
const handleAssignmentSelect = useCallback(
|
||||
(item: SelectionAssignmentMenuItem | null) => {
|
||||
(item: NormalizedSelectionAssignmentItem) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
@@ -271,11 +280,7 @@ export const TagSection: React.FC<TagSectionProps> = ({
|
||||
showCounts={false}
|
||||
positionStrategy="fixed"
|
||||
triggerClassName="quick-add__chip quick-add__trigger"
|
||||
triggerContent={(
|
||||
<span className="quick-add__chip-label">
|
||||
<PlusIcon className="icon-inline" aria-hidden="true" /> Add tag
|
||||
</span>
|
||||
)}
|
||||
triggerContent={<PlusIcon className="icon-inline" aria-hidden="true" />}
|
||||
/>
|
||||
) : null}
|
||||
{!tags.length && !showQuickAdd ? <span className="tag-list__empty meta">{emptyMessage}</span> : null}
|
||||
@@ -356,7 +361,7 @@ export const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
|
||||
}, [normalizedOptions, entries]);
|
||||
|
||||
const handleAssignmentSelect = useCallback(
|
||||
(item: SelectionAssignmentMenuItem | null) => {
|
||||
(item: NormalizedSelectionAssignmentItem) => {
|
||||
if (!onAdd || !item) {
|
||||
return;
|
||||
}
|
||||
@@ -411,11 +416,7 @@ export const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
|
||||
showCounts={false}
|
||||
positionStrategy="fixed"
|
||||
triggerClassName="quick-add__chip quick-add__trigger"
|
||||
triggerContent={(
|
||||
<span className="quick-add__chip-label">
|
||||
<PlusIcon className="icon-inline" aria-hidden="true" /> Add correspondent
|
||||
</span>
|
||||
)}
|
||||
triggerContent={<PlusIcon className="icon-inline" aria-hidden="true" />}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -435,20 +436,9 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
||||
onUpdateTitle,
|
||||
onUpdateIssued,
|
||||
layout = 'default',
|
||||
detailItems = [],
|
||||
}) => {
|
||||
const isCompactLayout = layout === 'compact';
|
||||
const summary = useMemo(() => {
|
||||
if (!document) {
|
||||
return {
|
||||
title: '',
|
||||
originalName: '',
|
||||
sizeLabel: '—',
|
||||
pageCount: null,
|
||||
};
|
||||
}
|
||||
return describeDocumentSummary(document);
|
||||
}, [document]);
|
||||
const summaryRows = useMemo(() => describeDocumentSummary(document), [document]);
|
||||
const issuedDateLabel = useMemo(
|
||||
() => formatDate(document?.issued_at, { fallback: null }),
|
||||
[document?.issued_at],
|
||||
@@ -475,8 +465,8 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
||||
return sortCorrespondents(document?.correspondents || []);
|
||||
}, [correspondents, document?.correspondents]);
|
||||
|
||||
const metaRows = useMemo(() => {
|
||||
const rows: { key: string; label: string; value: string | null }[] = [];
|
||||
const extraSummaryRows = useMemo(() => {
|
||||
const rows: DocumentSummaryRow[] = [];
|
||||
const currentVersionNumber = document?.current_version?.version_number;
|
||||
if (Number.isFinite(currentVersionNumber)) {
|
||||
rows.push({
|
||||
@@ -485,17 +475,8 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
||||
value: `#${currentVersionNumber}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (summary.sizeLabel && summary.sizeLabel !== '—') {
|
||||
rows.push({ key: 'size', label: 'Size', value: summary.sizeLabel });
|
||||
}
|
||||
|
||||
if (Number.isFinite(summary.pageCount)) {
|
||||
rows.push({ key: 'pages', label: 'Pages', value: String(summary.pageCount) });
|
||||
}
|
||||
|
||||
return rows;
|
||||
}, [document?.current_version?.version_number, summary]);
|
||||
}, [document?.current_version?.version_number]);
|
||||
|
||||
const [titleDraft, setTitleDraft] = useState('');
|
||||
const [titleSaving, setTitleSaving] = useState(false);
|
||||
@@ -595,57 +576,60 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const TitleSection = () => (
|
||||
editableTitle && isTitleEditing ? (
|
||||
<form className="doc-title-edit" onSubmit={submitTitleEdit}>
|
||||
<input
|
||||
value={titleDraft}
|
||||
onChange={(event) => {
|
||||
setTitleDraft(event.target.value);
|
||||
if (titleError) {
|
||||
setTitleError(null);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelTitleEdit();
|
||||
}
|
||||
}}
|
||||
aria-label="Document title"
|
||||
autoFocus
|
||||
disabled={titleSaving}
|
||||
/>
|
||||
<button type="submit" disabled={titleSaving}>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={cancelTitleEdit}
|
||||
disabled={titleSaving}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="doc-title-row__title">{summary.title}</h3>
|
||||
{editableTitle ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={startTitleEdit}
|
||||
aria-label="Edit title"
|
||||
title="Edit title"
|
||||
>
|
||||
<EditIcon className="icon-inline" />
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
const renderTitleEditForm = (extraClassName?: string) => (
|
||||
<form className={`doc-title-edit${extraClassName ? ` ${extraClassName}` : ''}`} onSubmit={submitTitleEdit}>
|
||||
<input
|
||||
value={titleDraft}
|
||||
onChange={(event) => {
|
||||
setTitleDraft(event.target.value);
|
||||
if (titleError) {
|
||||
setTitleError(null);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelTitleEdit();
|
||||
}
|
||||
}}
|
||||
aria-label="Document title"
|
||||
autoFocus
|
||||
disabled={titleSaving}
|
||||
/>
|
||||
<button type="submit" className="icon-button icon-button--accent" disabled={titleSaving} aria-label="Save title">
|
||||
<CheckIcon size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={cancelTitleEdit}
|
||||
disabled={titleSaving}
|
||||
aria-label="Cancel"
|
||||
>
|
||||
<IconX size={16} />
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
|
||||
const titleMetaDisplay = editableTitle && isTitleEditing
|
||||
? renderTitleEditForm('doc-title-edit--inline')
|
||||
: (
|
||||
<>
|
||||
<span className="detail-meta__value">{document?.title}</span>
|
||||
{editableTitle ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={startTitleEdit}
|
||||
aria-label="Edit title"
|
||||
title="Edit title"
|
||||
>
|
||||
<EditIcon className="icon-inline" />
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
const issuedDisplay = editableIssued && isIssuedEditing ? (
|
||||
<form className="doc-issued-edit" onSubmit={submitIssuedEdit}>
|
||||
<input
|
||||
@@ -660,16 +644,17 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
||||
aria-label="Issued on"
|
||||
disabled={issuedSaving}
|
||||
/>
|
||||
<button type="submit" disabled={issuedSaving}>
|
||||
Save
|
||||
<button type="submit" className="icon-button icon-button--accent" disabled={issuedSaving} aria-label="Save issued date">
|
||||
<CheckIcon size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
className="icon-button"
|
||||
onClick={cancelIssuedEdit}
|
||||
disabled={issuedSaving}
|
||||
aria-label="Cancel"
|
||||
>
|
||||
Cancel
|
||||
<IconX size={16} />
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
@@ -689,184 +674,98 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const metaItems = [
|
||||
{
|
||||
key: 'issued',
|
||||
label: 'Issued',
|
||||
valueContent: issuedDisplay,
|
||||
error: issuedError,
|
||||
},
|
||||
...metaRows.map((row) => ({
|
||||
key: row.key,
|
||||
label: row.label,
|
||||
fallbackValue: row.value,
|
||||
})),
|
||||
];
|
||||
|
||||
const detailRows = Array.isArray(detailItems)
|
||||
? detailItems.map((item, index) => ({
|
||||
key: `detail-${item?.label || index}`,
|
||||
label: item?.label || '—',
|
||||
fallbackValue: item?.value,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const compactRows = [...metaItems, ...detailRows];
|
||||
|
||||
const renderTags = () => (
|
||||
<section className="document-summary__section document-summary__section--tags">
|
||||
<TagSection
|
||||
tags={resolvedTags}
|
||||
onRemove={
|
||||
onTagRemove
|
||||
? (tag) => onTagRemove(document.id, tag.id)
|
||||
: undefined
|
||||
}
|
||||
onAdd={
|
||||
onTagAdd
|
||||
? ({ value, option }) => onTagAdd(document, value, { option })
|
||||
: undefined
|
||||
}
|
||||
datalistOptions={tagOptions}
|
||||
className="document-summary__tags"
|
||||
/>
|
||||
</section>
|
||||
const tagsValueContent = (
|
||||
<TagSection
|
||||
tags={resolvedTags}
|
||||
onRemove={
|
||||
onTagRemove
|
||||
? (tag) => onTagRemove(document.id, tag.id)
|
||||
: undefined
|
||||
}
|
||||
onAdd={
|
||||
onTagAdd
|
||||
? ({ value, option }) => onTagAdd(document, value, { option })
|
||||
: undefined
|
||||
}
|
||||
datalistOptions={tagOptions}
|
||||
className="document-summary__tags"
|
||||
/>
|
||||
);
|
||||
|
||||
const renderCorrespondents = () => (
|
||||
<section className="document-summary__section document-summary__section--correspondents">
|
||||
<CorrespondentSection
|
||||
entries={resolvedCorrespondents}
|
||||
onRemove={
|
||||
onCorrespondentRemove
|
||||
? (entry) =>
|
||||
onCorrespondentRemove({
|
||||
documentId: document.id,
|
||||
correspondentId: entry.id,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
onAdd={
|
||||
onCorrespondentAdd
|
||||
? ({ name, option }) =>
|
||||
onCorrespondentAdd({
|
||||
document,
|
||||
name,
|
||||
option,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
showCount
|
||||
datalistOptions={correspondentOptions}
|
||||
className="document-summary__correspondents"
|
||||
/>
|
||||
</section>
|
||||
const correspondentsValueContent = (
|
||||
<CorrespondentSection
|
||||
entries={resolvedCorrespondents}
|
||||
onRemove={
|
||||
onCorrespondentRemove
|
||||
? (entry) =>
|
||||
onCorrespondentRemove({
|
||||
documentId: document.id,
|
||||
correspondentId: entry.id,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
onAdd={
|
||||
onCorrespondentAdd
|
||||
? ({ name, option }) =>
|
||||
onCorrespondentAdd({
|
||||
document,
|
||||
name,
|
||||
option,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
showCount
|
||||
datalistOptions={correspondentOptions}
|
||||
className="document-summary__correspondents"
|
||||
/>
|
||||
);
|
||||
|
||||
if (isCompactLayout) {
|
||||
return (
|
||||
<div className="document-summary document-summary--compact">
|
||||
<section className="document-summary__section document-summary__title-row">
|
||||
<TitleSection />
|
||||
</section>
|
||||
{titleError ? <div className="status-inline error">{titleError}</div> : null}
|
||||
{renderTags()}
|
||||
{renderCorrespondents()}
|
||||
{compactRows.length ? (
|
||||
<section className="document-summary__section document-summary__meta document-summary__meta--compact">
|
||||
<dl className="document-summary__details-list document-summary__details-list--meta">
|
||||
{compactRows.map((item) => (
|
||||
<div key={item.key} className="document-summary__details-row">
|
||||
<dt>{item.label}</dt>
|
||||
<dd>
|
||||
{item.valueContent != null && item.valueContent !== ''
|
||||
? item.valueContent
|
||||
: item.fallbackValue || '—'}
|
||||
</dd>
|
||||
{item.error ? <div className="status-inline error">{item.error}</div> : null}
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const summaryRowOverrides = useMemo(
|
||||
() => ({
|
||||
title: { valueContent: titleMetaDisplay, error: titleError },
|
||||
issued: { valueContent: issuedDisplay, error: issuedError },
|
||||
tags: { valueContent: tagsValueContent },
|
||||
correspondents: { valueContent: correspondentsValueContent },
|
||||
}),
|
||||
[titleMetaDisplay, titleError, issuedDisplay, issuedError, tagsValueContent, correspondentsValueContent],
|
||||
);
|
||||
|
||||
const baseRows: MetaItem[] = useMemo(
|
||||
() => [...summaryRows, ...extraSummaryRows].map((row) => {
|
||||
const overrides = summaryRowOverrides[row.key] || {};
|
||||
return {
|
||||
key: row.key,
|
||||
label: row.label,
|
||||
valueContent: overrides.valueContent ?? null,
|
||||
fallbackValue: overrides.valueContent ? row.value : row.value,
|
||||
error: overrides.error ?? null,
|
||||
};
|
||||
}),
|
||||
[summaryRows, extraSummaryRows, summaryRowOverrides],
|
||||
);
|
||||
|
||||
const allRows = baseRows;
|
||||
const summaryClass = `document-summary${isCompactLayout ? ' document-summary--compact' : ''}`;
|
||||
const sectionClass = `document-summary__section document-summary__meta${isCompactLayout ? ' document-summary__meta--compact' : ''}`;
|
||||
const listClass = `document-summary__details-list${isCompactLayout ? ' document-summary__details-list--meta' : ''}`;
|
||||
|
||||
return (
|
||||
<div className="document-summary">
|
||||
<div className="doc-title-row">
|
||||
<div className="doc-title-row__primary">
|
||||
{editableTitle && isTitleEditing ? (
|
||||
<form className="doc-title-edit" onSubmit={submitTitleEdit}>
|
||||
<input
|
||||
value={titleDraft}
|
||||
onChange={(event) => {
|
||||
setTitleDraft(event.target.value);
|
||||
if (titleError) {
|
||||
setTitleError(null);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelTitleEdit();
|
||||
}
|
||||
}}
|
||||
aria-label="Document title"
|
||||
autoFocus
|
||||
disabled={titleSaving}
|
||||
/>
|
||||
<button type="submit" disabled={titleSaving}>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={cancelTitleEdit}
|
||||
disabled={titleSaving}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="doc-title-row__title">{summary.title}</h3>
|
||||
{editableTitle ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={startTitleEdit}
|
||||
aria-label="Edit title"
|
||||
title="Edit title"
|
||||
>
|
||||
<EditIcon className="icon-inline" />
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{titleError ? <div className="status-inline error">{titleError}</div> : null}
|
||||
|
||||
<div className="detail-meta">
|
||||
<div className="detail-meta__row">
|
||||
<span className="detail-meta__label">Issued:</span>
|
||||
{issuedDisplay}
|
||||
</div>
|
||||
{issuedError ? <div className="status-inline error">{issuedError}</div> : null}
|
||||
|
||||
{metaRows.map((row) => (
|
||||
<div key={row.key} className="detail-meta__row">
|
||||
<span className="detail-meta__label">{row.label}:</span>
|
||||
<span className="detail-meta__value">{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{renderTags()}
|
||||
{renderCorrespondents()}
|
||||
<div className={summaryClass}>
|
||||
<section className={sectionClass}>
|
||||
<dl className={listClass}>
|
||||
{allRows.map((item) => (
|
||||
<div key={item.key} className="document-summary__details-row">
|
||||
<dt>{item.label}</dt>
|
||||
<dd>
|
||||
{item.valueContent != null && item.valueContent !== ''
|
||||
? item.valueContent
|
||||
: item.fallbackValue || '—'}
|
||||
</dd>
|
||||
{item.error ? <div className="status-inline error">{item.error}</div> : null}
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,10 +77,10 @@ interface DocumentsGridProps {
|
||||
getDocumentAsset?: (...args: any[]) => unknown;
|
||||
gridIconSize?: number;
|
||||
tagLookupById?: Map<Identifier, DocumentTag> | null;
|
||||
onTagClick?: (tagId?: Identifier | null) => void;
|
||||
onTagClick?: (tagId: Identifier) => void;
|
||||
scrollRef?: RefObject<HTMLElement | null>;
|
||||
onCorrespondentClick?: (correspondentId?: Identifier | null) => void;
|
||||
activeCorrespondentIdSet?: Set<Identifier | null> | null;
|
||||
onCorrespondentClick?: (correspondentId: Identifier) => void;
|
||||
activeCorrespondentIdSet?: Set<Identifier> | null;
|
||||
onDocumentRename?: (docId: Identifier, title: string) => Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
@@ -434,20 +434,26 @@ const DocumentsGrid: React.FC<DocumentsGridProps> = ({
|
||||
</div>
|
||||
{visibleTags.length > 0 && (
|
||||
<div className="document-card__tags">
|
||||
{visibleTags.map((tag) => {
|
||||
{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 ?? `${doc.id}-tag-${index}`;
|
||||
return (
|
||||
<span
|
||||
key={tag.id}
|
||||
key={key}
|
||||
className="badge tag-chip"
|
||||
style={style || undefined}
|
||||
title={tag.label}
|
||||
role="button"
|
||||
onClick={(event) => {
|
||||
role={clickable ? 'button' : undefined}
|
||||
onClick={clickable ? (event) => {
|
||||
event.stopPropagation();
|
||||
onTagClick?.(tag.id);
|
||||
}}
|
||||
if (tagId == null) {
|
||||
return;
|
||||
}
|
||||
onTagClick?.(tagId);
|
||||
} : undefined}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
event.stopPropagation();
|
||||
@@ -463,13 +469,16 @@ const DocumentsGrid: React.FC<DocumentsGridProps> = ({
|
||||
onDragEnd={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
onKeyDown={clickable ? (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onTagClick?.(tag.id);
|
||||
if (tagId == null) {
|
||||
return;
|
||||
}
|
||||
onTagClick?.(tagId);
|
||||
}
|
||||
}}
|
||||
} : undefined}
|
||||
>
|
||||
{tag.label}
|
||||
</span>
|
||||
|
||||
@@ -82,9 +82,9 @@ export interface DocumentsListProps {
|
||||
onDocumentTagDrop?: (event: DragEvent<HTMLTableRowElement>, documentId: Identifier) => void;
|
||||
onDocumentRename?: (documentId: Identifier, nextTitle: string) => Promise<boolean> | boolean;
|
||||
tagLookupById?: Map<Identifier, DocumentTag> | null;
|
||||
onTagClick?: (tagId?: Identifier | null) => void;
|
||||
onCorrespondentClick?: (correspondentId?: Identifier | null) => void;
|
||||
activeCorrespondentIdSet?: Set<Identifier | null> | null;
|
||||
onTagClick?: (tagId: Identifier) => void;
|
||||
onCorrespondentClick?: (correspondentId: Identifier) => void;
|
||||
activeCorrespondentIdSet?: Set<Identifier> | null;
|
||||
scrollRef?: RefObject<HTMLElement | null>;
|
||||
}
|
||||
|
||||
@@ -456,20 +456,24 @@ const DocumentsList: React.FC<DocumentsListProps> = ({
|
||||
</div>
|
||||
{(doc.tags || []).length > 0 && (
|
||||
<div className="doc-name__tags">
|
||||
{(doc.tags || []).map((tag) => {
|
||||
{(doc.tags || []).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 ?? `${doc.id}-tag-${index}`;
|
||||
return (
|
||||
<span
|
||||
key={tag.id}
|
||||
key={key}
|
||||
className="badge tag-chip"
|
||||
style={style || undefined}
|
||||
title={tag.label}
|
||||
role="button"
|
||||
onClick={(event) => {
|
||||
role={clickable ? 'button' : undefined}
|
||||
onClick={clickable ? (event) => {
|
||||
event.stopPropagation();
|
||||
onTagClick?.(tag.id);
|
||||
}}
|
||||
if (tagId == null) return;
|
||||
onTagClick?.(tagId);
|
||||
} : undefined}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
event.stopPropagation();
|
||||
@@ -485,13 +489,16 @@ const DocumentsList: React.FC<DocumentsListProps> = ({
|
||||
onDragEnd={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
onKeyDown={clickable ? (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onTagClick?.(tag.id);
|
||||
if (tagId == null) {
|
||||
return;
|
||||
}
|
||||
onTagClick?.(tagId);
|
||||
}
|
||||
}}
|
||||
} : undefined}
|
||||
>
|
||||
{tag.label}
|
||||
</span>
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface SelectionAssignmentMenuProps {
|
||||
items?: SelectionAssignmentMenuItem[];
|
||||
placeholder?: string;
|
||||
emptyMessage?: string;
|
||||
createLabel?: string | null;
|
||||
createLabel?: string;
|
||||
onToggle?: (item: NormalizedSelectionAssignmentItem) => Promise<void> | void;
|
||||
onCreate?: (value: string) => Promise<void> | void;
|
||||
disabled?: boolean;
|
||||
@@ -82,7 +82,7 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
|
||||
items = [],
|
||||
placeholder = 'Search…',
|
||||
emptyMessage = 'No entries',
|
||||
createLabel = null,
|
||||
createLabel = 'Add',
|
||||
onToggle,
|
||||
onCreate,
|
||||
disabled = false,
|
||||
@@ -91,8 +91,8 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
|
||||
triggerClassName = 'quick-add__chip quick-add__trigger panel-floating-actions__trigger',
|
||||
showStateIndicators = true,
|
||||
showCounts = true,
|
||||
onOpenMenu = null,
|
||||
renderItemLabel = null,
|
||||
onOpenMenu,
|
||||
renderItemLabel,
|
||||
positionStrategy = 'absolute',
|
||||
}) => {
|
||||
const anchorRef = useRef<HTMLButtonElement | null>(null);
|
||||
@@ -162,7 +162,7 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
|
||||
|
||||
const handleToggle = useCallback(
|
||||
async (item: NormalizedSelectionAssignmentItem) => {
|
||||
if (!item || !onToggle) {
|
||||
if (!onToggle) {
|
||||
return;
|
||||
}
|
||||
setPending(true);
|
||||
@@ -265,8 +265,8 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
|
||||
type="submit"
|
||||
className="icon-button selection-assignment__add"
|
||||
disabled={!canSubmitCreate}
|
||||
aria-label={createLabel || 'Add'}
|
||||
title={createLabel || 'Add'}
|
||||
aria-label={createLabel}
|
||||
title={createLabel}
|
||||
>
|
||||
<PlusIcon aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -12,18 +12,18 @@ export type DocumentLike = OcrDocumentLike;
|
||||
|
||||
const asyncFalse = async () => false;
|
||||
|
||||
const resolveDocumentDownloadHref = (document: DocumentLike | null | undefined, resolveApiPath?: ResolveApiPath | null): string | null => {
|
||||
const resolveDocumentDownloadHref = (document?: DocumentLike | null, resolveApiPath?: ResolveApiPath | null): string | null => {
|
||||
if (!document || !resolveApiPath) {
|
||||
return null;
|
||||
}
|
||||
const downloadPath = (document.current_version as { download_path?: string | null } | null | undefined)?.download_path;
|
||||
const downloadPath = (document.current_version as { download_path?: string | null } | null)?.download_path;
|
||||
if (!downloadPath) {
|
||||
return null;
|
||||
}
|
||||
return resolveApiPath(downloadPath);
|
||||
};
|
||||
|
||||
const hasDocumentOcrAsset = (document: DocumentLike | null | undefined, getDocumentAsset?: GetDocumentAsset | null): boolean => {
|
||||
const hasDocumentOcrAsset = (document?: DocumentLike | null, getDocumentAsset?: GetDocumentAsset | null): boolean => {
|
||||
if (!document || !getDocumentAsset) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { formatDateTime } from '../utils/date';
|
||||
|
||||
interface DocumentVersionMetadata {
|
||||
checksum?: string | null;
|
||||
}
|
||||
|
||||
interface DocumentMetadata {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface DocumentLike {
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
filename?: string | null;
|
||||
original_name?: string | null;
|
||||
content_type?: string | null;
|
||||
metadata?: DocumentMetadata | null;
|
||||
current_version?: DocumentVersionMetadata | null;
|
||||
}
|
||||
|
||||
export interface DocumentMetadataItem {
|
||||
label: string;
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
export const buildDocumentMetadataItems = (document?: DocumentLike | null): DocumentMetadataItem[] => {
|
||||
if (!document) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const metadata = document.current_version || {};
|
||||
|
||||
return [
|
||||
{ label: 'Created at', value: formatDateTime(document.created_at) },
|
||||
{ label: 'Updated at', value: formatDateTime(document.updated_at) },
|
||||
{
|
||||
label: 'Filename',
|
||||
value: document.filename ?? null,
|
||||
},
|
||||
{
|
||||
label: 'Original filename',
|
||||
value: document.original_name ?? null,
|
||||
},
|
||||
{
|
||||
label: 'SHA-256 checksum',
|
||||
value: metadata.checksum ?? null,
|
||||
},
|
||||
{
|
||||
label: 'Content type',
|
||||
value: document.content_type ?? null,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const extractDocumentMetadataPayload = (document?: DocumentLike | null): DocumentMetadata | null => {
|
||||
if (!document?.metadata) {
|
||||
return null;
|
||||
}
|
||||
const keys = Object.keys(document.metadata);
|
||||
if (!keys.length) {
|
||||
return null;
|
||||
}
|
||||
return document.metadata;
|
||||
};
|
||||
|
||||
export default buildDocumentMetadataItems;
|
||||
@@ -1,13 +1,13 @@
|
||||
import { formatFileSize } from '../utils/format';
|
||||
import { formatDateTime as defaultFormatDateTime } from '../utils/date';
|
||||
|
||||
interface DocumentMetadata {
|
||||
interface DocumentPageMetadata {
|
||||
page_count?: number | string | null;
|
||||
}
|
||||
|
||||
interface DocumentVersion {
|
||||
size_bytes?: number | string | null;
|
||||
metadata?: DocumentMetadata | null;
|
||||
metadata?: DocumentPageMetadata | null;
|
||||
}
|
||||
|
||||
interface TagEntry {
|
||||
@@ -35,31 +35,18 @@ interface DescribeSummaryOptions {
|
||||
formatDateTime?: typeof defaultFormatDateTime;
|
||||
}
|
||||
|
||||
export type DocumentSummaryRowType = 'text' | 'editable-title' | 'editable-issued' | 'tags' | 'correspondents';
|
||||
|
||||
export interface DocumentSummaryRow {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string | null;
|
||||
kind?: DocumentSummaryRowType;
|
||||
}
|
||||
|
||||
export interface DocumentSummary {
|
||||
title: string | undefined;
|
||||
originalName: string | null;
|
||||
mimeTypeLabel: string;
|
||||
sizeLabel: string;
|
||||
createdAtLabel: string;
|
||||
issuedLabel: string;
|
||||
updatedAtLabel: string;
|
||||
pageCount: number | null;
|
||||
pageCountLabel: string;
|
||||
folderLabel: string | null;
|
||||
tags: TagEntry[];
|
||||
correspondents: CorrespondentEntry[];
|
||||
tagsSummary: string;
|
||||
correspondentsSummary: string;
|
||||
summaryRows: DocumentSummaryRow[];
|
||||
}
|
||||
export type DocumentSummary = DocumentSummaryRow[];
|
||||
|
||||
const coercePageCount = (metadata?: DocumentMetadata | null): number | null => {
|
||||
const coercePageCount = (metadata?: DocumentPageMetadata | null): number | null => {
|
||||
const raw = metadata?.page_count;
|
||||
if (raw == null || raw === '') {
|
||||
return null;
|
||||
@@ -71,86 +58,61 @@ const coercePageCount = (metadata?: DocumentMetadata | null): number | null => {
|
||||
const sanitizeArray = <T>(entries?: Array<T | null> | null): T[] =>
|
||||
Array.isArray(entries) ? entries.filter(Boolean) as T[] : [];
|
||||
|
||||
interface DocumentMetadataPayload {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface MetadataDocumentLike {
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
filename?: string | null;
|
||||
original_name?: string | null;
|
||||
content_type?: string | null;
|
||||
metadata?: DocumentMetadataPayload | null;
|
||||
current_version?: { checksum?: string | null } | null;
|
||||
}
|
||||
|
||||
export const describeDocumentSummary = (document?: SummaryDocument | null, options: DescribeSummaryOptions = {}): DocumentSummary => {
|
||||
const {
|
||||
formatDateTime = defaultFormatDateTime,
|
||||
} = options;
|
||||
|
||||
if (!document) {
|
||||
return {
|
||||
title: '',
|
||||
originalName: null,
|
||||
mimeTypeLabel: '—',
|
||||
sizeLabel: '—',
|
||||
createdAtLabel: '—',
|
||||
issuedLabel: '—',
|
||||
updatedAtLabel: '—',
|
||||
pageCount: null,
|
||||
pageCountLabel: '—',
|
||||
folderLabel: null,
|
||||
tags: [],
|
||||
correspondents: [],
|
||||
tagsSummary: '—',
|
||||
correspondentsSummary: '—',
|
||||
summaryRows: [],
|
||||
};
|
||||
}
|
||||
|
||||
const originalName = document.original_name;
|
||||
const mimeTypeLabel = document.content_type || 'Unknown';
|
||||
|
||||
const sizeBytes = Number(document.current_version?.size_bytes);
|
||||
const formatDateLabel = (value?: string | null) => formatDateTime(value) || '—';
|
||||
const doc = document ?? {};
|
||||
const sizeBytes = Number(doc.current_version?.size_bytes);
|
||||
const sizeLabel = Number.isFinite(sizeBytes) && sizeBytes > 0 ? formatFileSize(sizeBytes) : '—';
|
||||
|
||||
const metadata = document.current_version?.metadata || null;
|
||||
const metadata = doc.current_version?.metadata || null;
|
||||
const pageCount = coercePageCount(metadata);
|
||||
const pageCountLabel = Number.isFinite(pageCount) ? String(pageCount) : '—';
|
||||
|
||||
const createdAtLabel = formatDateTime(document.created_at);
|
||||
const issuedLabel = formatDateTime(document.issued_at);
|
||||
const updatedAtLabel = formatDateTime(document.updated_at);
|
||||
|
||||
const folderLabel = document.folder_path ?? null;
|
||||
const displayFolderLabel = folderLabel ?? 'Documents';
|
||||
|
||||
const tags = sanitizeArray<TagEntry>(document.tags);
|
||||
const correspondents = sanitizeArray<CorrespondentEntry>(document.correspondents);
|
||||
|
||||
const tags = sanitizeArray<TagEntry>(doc.tags);
|
||||
const correspondents = sanitizeArray<CorrespondentEntry>(doc.correspondents);
|
||||
const tagLabels = tags.map((tag) => tag.label).filter(Boolean) as string[];
|
||||
const correspondentLabels = correspondents.map((entry) => entry.name).filter(Boolean) as string[];
|
||||
|
||||
const tagsSummary = tagLabels.length ? tagLabels.join(', ') : '—';
|
||||
const correspondentsSummary = correspondentLabels.length
|
||||
? correspondentLabels.join(', ')
|
||||
: '—';
|
||||
|
||||
const summaryRows: DocumentSummaryRow[] = [
|
||||
{ key: 'created', label: 'Created', value: createdAtLabel },
|
||||
const correspondentsSummary = correspondentLabels.length ? correspondentLabels.join(', ') : '—';
|
||||
return [
|
||||
{ key: 'title', label: 'Title', value: doc.title ?? null, kind: 'editable-title' },
|
||||
{ key: 'tags', label: 'Tags', value: tagsSummary, kind: 'tags' },
|
||||
{ key: 'correspondents', label: 'Correspondents', value: correspondentsSummary, kind: 'correspondents' },
|
||||
{ key: 'issued', label: 'Issued', value: formatDateLabel(doc.issued_at), kind: 'editable-issued' },
|
||||
{ key: 'created', label: 'Created at', value: formatDateLabel(doc.created_at) },
|
||||
{ key: 'updated', label: 'Updated at', value: formatDateLabel(doc.updated_at) },
|
||||
{ key: 'size', label: 'Size', value: sizeLabel },
|
||||
{ key: 'type', label: 'Type', value: mimeTypeLabel },
|
||||
{ key: 'issued', label: 'Issued', value: issuedLabel },
|
||||
{ key: 'content-type', label: 'Content type', value: doc.content_type || 'Unknown' },
|
||||
{ key: 'pages', label: 'Pages', value: pageCountLabel },
|
||||
{ key: 'updated', label: 'Updated', value: updatedAtLabel },
|
||||
{ key: 'folder', label: 'Folder', value: displayFolderLabel },
|
||||
{ key: 'tags', label: 'Tags', value: tagsSummary },
|
||||
{ key: 'correspondents', label: 'Correspondents', value: correspondentsSummary },
|
||||
{ key: 'filename', label: 'Filename', value: doc.filename },
|
||||
{ key: 'original-filename', label: 'Original filename', value: doc.original_name },
|
||||
{ key: 'checksum', label: 'SHA-256 checksum', value: doc.current_version?.checksum },
|
||||
];
|
||||
};
|
||||
|
||||
return {
|
||||
title: document.title,
|
||||
originalName,
|
||||
mimeTypeLabel,
|
||||
sizeLabel,
|
||||
createdAtLabel,
|
||||
issuedLabel,
|
||||
updatedAtLabel,
|
||||
pageCount,
|
||||
pageCountLabel,
|
||||
folderLabel,
|
||||
tags,
|
||||
correspondents,
|
||||
tagsSummary,
|
||||
correspondentsSummary,
|
||||
summaryRows,
|
||||
};
|
||||
export const extractDocumentMetadataPayload = (document?: MetadataDocumentLike | null): DocumentMetadataPayload | null => {
|
||||
if (!document?.metadata) {
|
||||
return null;
|
||||
}
|
||||
const keys = Object.keys(document.metadata);
|
||||
if (!keys.length) {
|
||||
return null;
|
||||
}
|
||||
return document.metadata;
|
||||
};
|
||||
|
||||
@@ -20,8 +20,8 @@ interface UseDocumentsSelectionOptions {
|
||||
showingSearchResults: boolean;
|
||||
currentSubfolders: FolderEntry[];
|
||||
visibleDocuments: DocumentEntry[];
|
||||
resolveFolderRowKey: (id: string | number) => string | null | undefined;
|
||||
resolveDocumentRowKey: (id: string | number) => string | null | undefined;
|
||||
resolveFolderRowKey: (id: string | number) => string | null;
|
||||
resolveDocumentRowKey: (id: string | number) => string | null;
|
||||
configureSelectionEnvironment: (config: { visibleRowKeySet: Set<string>; navigableRowKeys: string[] }) => void;
|
||||
visibleRowKeySet: Set<string>;
|
||||
selectedEntries: string[];
|
||||
|
||||
@@ -12,8 +12,8 @@ type FocusableInput = (HTMLInputElement | HTMLTextAreaElement) & {
|
||||
};
|
||||
|
||||
type InlineRenameOptions<TEntity> = {
|
||||
getCurrentValue?: (entity: TEntity) => string | null | undefined;
|
||||
getEntityId?: (entity: TEntity) => string | number | null | undefined;
|
||||
getCurrentValue?: (entity: TEntity) => string | null;
|
||||
getEntityId?: (entity: TEntity) => string | number | null;
|
||||
};
|
||||
|
||||
type InlineRenameHandler = (
|
||||
@@ -25,9 +25,9 @@ type InlineRenameReturn<TEntity> = {
|
||||
editingId: string | number | null;
|
||||
draftValue: string;
|
||||
setDraftValue: Dispatch<SetStateAction<string>>;
|
||||
beginEditing: (entity: TEntity | null | undefined, event?: SyntheticEvent | Event) => void;
|
||||
beginEditing: (entity?: TEntity | null, event?: SyntheticEvent | Event) => void;
|
||||
cancelEditing: (event?: SyntheticEvent | Event) => void;
|
||||
submitEditing: (entity: TEntity | null | undefined) => Promise<boolean>;
|
||||
submitEditing: (entity?: TEntity | null) => Promise<boolean>;
|
||||
savingId: string | number | null;
|
||||
attachInputRef: (node: FocusableInput | null) => void;
|
||||
};
|
||||
@@ -50,16 +50,14 @@ const focusInput = (node: FocusableInput | null) => {
|
||||
|
||||
const identity = (value: unknown) => value as string;
|
||||
|
||||
const defaultGetEntityId = <T,>(entity: T) =>
|
||||
(entity as { id?: string | number } | null | undefined)?.id ?? null;
|
||||
const defaultGetEntityId = <T,>(entity?: T | null) =>
|
||||
(entity as { id?: string | number } | null)?.id ?? null;
|
||||
|
||||
const useInlineRename = <TEntity,>(
|
||||
onRename?: InlineRenameHandler,
|
||||
{
|
||||
getCurrentValue = identity as (entity: TEntity) => string | null | undefined,
|
||||
getEntityId = defaultGetEntityId as (
|
||||
entity: TEntity,
|
||||
) => string | number | null | undefined,
|
||||
getCurrentValue = identity as (entity: TEntity) => string | null,
|
||||
getEntityId = defaultGetEntityId as (entity: TEntity) => string | number | null,
|
||||
}: InlineRenameOptions<TEntity> = {},
|
||||
): InlineRenameReturn<TEntity> => {
|
||||
const [editingId, setEditingId] = useState<string | number | null>(null);
|
||||
@@ -75,7 +73,7 @@ const useInlineRename = <TEntity,>(
|
||||
}, []);
|
||||
|
||||
const beginEditing = useCallback(
|
||||
(entity: TEntity | null | undefined, event?: SyntheticEvent | Event) => {
|
||||
(entity?: TEntity | null, event?: SyntheticEvent | Event) => {
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
@@ -107,7 +105,7 @@ const useInlineRename = <TEntity,>(
|
||||
);
|
||||
|
||||
const submitEditing = useCallback(
|
||||
async (entity: TEntity | null | undefined) => {
|
||||
async (entity?: TEntity | null) => {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user