desktop redo
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import DocumentSummarySection from './DocumentSummarySection';
|
||||
import { buildDocumentMetadataItems, extractDocumentMetadataPayload } from './documentMetadata';
|
||||
|
||||
const DocumentInfoPanel = ({
|
||||
document,
|
||||
summaryProps = {},
|
||||
metadataItems: metadataItemsProp,
|
||||
metadataPayload: metadataPayloadProp,
|
||||
metadataTabLabel = 'Metadata',
|
||||
detailsTabLabel = 'Details',
|
||||
contentConfig: contentConfigProp = null,
|
||||
activeTab: controlledActiveTab,
|
||||
onTabChange,
|
||||
defaultTabId = 'details',
|
||||
resetKey = null,
|
||||
classNamePrefix = 'document-info',
|
||||
hideTabNavWhenSingle = true,
|
||||
}) => {
|
||||
const base = classNamePrefix;
|
||||
|
||||
const metadataItems = useMemo(() => {
|
||||
if (Array.isArray(metadataItemsProp) && metadataItemsProp.length) {
|
||||
return metadataItemsProp;
|
||||
}
|
||||
return buildDocumentMetadataItems(document);
|
||||
}, [metadataItemsProp, document]);
|
||||
|
||||
const metadataPayload = useMemo(() => {
|
||||
if (metadataPayloadProp !== undefined) {
|
||||
return metadataPayloadProp;
|
||||
}
|
||||
return extractDocumentMetadataPayload(document);
|
||||
}, [metadataPayloadProp, document]);
|
||||
|
||||
const contentConfig = contentConfigProp || null;
|
||||
const contentEnabled = Boolean(contentConfig && (contentConfig.enabled ?? true));
|
||||
const showContentTab = Boolean(contentConfig && ((contentConfig.forceDisplay ?? contentEnabled)));
|
||||
|
||||
const [contentState, setContentState] = useState(() => {
|
||||
if (!contentConfig) {
|
||||
return null;
|
||||
}
|
||||
if (!contentEnabled || typeof contentConfig.loadContent !== 'function') {
|
||||
return { status: contentEnabled ? 'idle' : 'unavailable', data: null, error: null };
|
||||
}
|
||||
return { status: 'idle', data: null, error: null };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!contentConfig || !showContentTab) {
|
||||
setContentState(null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!contentEnabled || typeof contentConfig.loadContent !== 'function') {
|
||||
setContentState({ status: contentEnabled ? 'idle' : 'unavailable', data: null, error: null });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const controller = new AbortController();
|
||||
|
||||
setContentState({ status: 'loading', data: null, error: null });
|
||||
|
||||
Promise.resolve(contentConfig.loadContent({ signal: controller.signal }))
|
||||
.then((result) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
if (result && result.length) {
|
||||
setContentState({ status: 'loaded', data: result, error: null });
|
||||
} else {
|
||||
setContentState({ status: 'empty', data: '', error: null });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled || error?.name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
setContentState({
|
||||
status: 'error',
|
||||
data: null,
|
||||
error,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
controller.abort();
|
||||
contentConfig.onCancel?.();
|
||||
};
|
||||
}, [contentConfig, contentEnabled, showContentTab, document?.id, resetKey]);
|
||||
|
||||
const visibleTabs = useMemo(() => {
|
||||
const tabsList = [];
|
||||
|
||||
tabsList.push({
|
||||
id: 'details',
|
||||
label: detailsTabLabel,
|
||||
render: () => (
|
||||
<section className={`${base}__section`}>
|
||||
{metadataItems.length ? (
|
||||
<dl className={`${base}__section-list`}>
|
||||
{metadataItems.map(({ label, value }) => (
|
||||
<div className={`${base}__section-item`} key={label}>
|
||||
<dt>{label}</dt>
|
||||
<dd>{value || '—'}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : (
|
||||
<p className={`${base}__section-placeholder`}>No details available.</p>
|
||||
)}
|
||||
</section>
|
||||
),
|
||||
});
|
||||
|
||||
if (showContentTab && contentConfig) {
|
||||
tabsList.push({
|
||||
id: contentConfig.id || 'content',
|
||||
label: contentConfig.label || 'Content',
|
||||
render: () => {
|
||||
const messageClass = `${base}__message`;
|
||||
const errorClass = `${base}__message ${base}__message--error`;
|
||||
const objectClass = `${base}__object ${base}__object--ocr-text`;
|
||||
|
||||
if (!contentEnabled || !contentConfig.loadContent) {
|
||||
return (
|
||||
<div className={messageClass}>
|
||||
{contentConfig.unavailableMessage || 'Content not available.'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!contentState) {
|
||||
return (
|
||||
<div className={messageClass}>
|
||||
{contentConfig.emptyMessage || 'No content available.'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (contentState.status) {
|
||||
case 'loading':
|
||||
return (
|
||||
<div className={messageClass}>
|
||||
{contentConfig.loadingMessage || 'Loading content…'}
|
||||
</div>
|
||||
);
|
||||
case 'error': {
|
||||
const errorMessage =
|
||||
contentConfig.errorMessage
|
||||
|| (contentState.error instanceof Error ? contentState.error.message : null)
|
||||
|| 'Failed to load content.';
|
||||
return <div className={errorClass}>{errorMessage}</div>;
|
||||
}
|
||||
case 'empty':
|
||||
return (
|
||||
<div className={messageClass}>
|
||||
{contentConfig.emptyMessage || 'No content available.'}
|
||||
</div>
|
||||
);
|
||||
case 'loaded':
|
||||
return (
|
||||
<pre className={objectClass}>{contentState.data}</pre>
|
||||
);
|
||||
case 'unavailable':
|
||||
return (
|
||||
<div className={messageClass}>
|
||||
{contentConfig.unavailableMessage || 'Content not available.'}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className={messageClass}>
|
||||
{contentConfig.emptyMessage || 'No content available.'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (metadataPayload) {
|
||||
tabsList.push({
|
||||
id: 'metadata',
|
||||
label: metadataTabLabel,
|
||||
render: () => (
|
||||
<section className={`${base}__section ${base}__section--metadata-json`}>
|
||||
<pre className={`${base}__metadata-json`}>
|
||||
{JSON.stringify(metadataPayload, null, 2)}
|
||||
</pre>
|
||||
</section>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return tabsList;
|
||||
}, [
|
||||
base,
|
||||
detailsTabLabel,
|
||||
metadataItems,
|
||||
contentConfig,
|
||||
contentEnabled,
|
||||
contentState,
|
||||
metadataPayload,
|
||||
metadataTabLabel,
|
||||
showContentTab,
|
||||
]);
|
||||
|
||||
const fallbackTabId = useMemo(() => {
|
||||
if (!visibleTabs.length) {
|
||||
return null;
|
||||
}
|
||||
if (defaultTabId && visibleTabs.some((tab) => tab.id === defaultTabId)) {
|
||||
return defaultTabId;
|
||||
}
|
||||
return visibleTabs[0].id;
|
||||
}, [visibleTabs, defaultTabId]);
|
||||
|
||||
const isControlled = controlledActiveTab !== undefined && controlledActiveTab !== null;
|
||||
const [uncontrolledTab, setUncontrolledTab] = useState(
|
||||
isControlled ? controlledActiveTab : fallbackTabId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isControlled) {
|
||||
setUncontrolledTab(fallbackTabId);
|
||||
}
|
||||
}, [fallbackTabId, resetKey, isControlled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled && controlledActiveTab && !visibleTabs.some((tab) => tab.id === controlledActiveTab)) {
|
||||
const nextTab = fallbackTabId;
|
||||
if (nextTab && nextTab !== controlledActiveTab) {
|
||||
onTabChange?.(nextTab);
|
||||
}
|
||||
}
|
||||
}, [isControlled, controlledActiveTab, visibleTabs, fallbackTabId, onTabChange]);
|
||||
|
||||
const activeTabId = isControlled ? controlledActiveTab : uncontrolledTab;
|
||||
|
||||
const handleTabSelect = (tabId) => {
|
||||
if (!visibleTabs.some((tab) => tab.id === tabId)) {
|
||||
return;
|
||||
}
|
||||
if (!isControlled) {
|
||||
setUncontrolledTab(tabId);
|
||||
}
|
||||
if (tabId !== activeTabId) {
|
||||
onTabChange?.(tabId);
|
||||
}
|
||||
};
|
||||
|
||||
const singleTab = visibleTabs.length === 1 ? visibleTabs[0] : null;
|
||||
const shouldHideNav = hideTabNavWhenSingle && singleTab;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentSummarySection
|
||||
document={document}
|
||||
{...summaryProps}
|
||||
/>
|
||||
{shouldHideNav ? (
|
||||
<div className={`${base}__tabpanes ${base}__tabpanes--single`}>
|
||||
<div className={`${base}__tabpanel`}>
|
||||
{renderTabContent(singleTab, { document })}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${base}__tabs-wrapper`}>
|
||||
<div className={`${base}__tabs`} role="tablist" aria-label="Document details">
|
||||
{visibleTabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab.id === activeTabId}
|
||||
className={`${base}__tab${tab.id === activeTabId ? ' is-active' : ''}`}
|
||||
onClick={() => handleTabSelect(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${base}__tabpanes`}>
|
||||
{visibleTabs.map((tab) => (
|
||||
tab.id === activeTabId ? (
|
||||
<div key={tab.id} role="tabpanel" className={`${base}__tabpanel`}>
|
||||
{renderTabContent(tab, { document })}
|
||||
</div>
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentInfoPanel;
|
||||
Reference in New Issue
Block a user