Commit 86bbaf04 authored by drigle's avatar drigle

fix: use modal confirmations and adjust filter tree defaults

parent a288b353
...@@ -34,13 +34,23 @@ function useSubjectRecords(subjectName, groupName) { ...@@ -34,13 +34,23 @@ function useSubjectRecords(subjectName, groupName) {
function ProcessListView({ subjectName, config }) { function ProcessListView({ subjectName, config }) {
const groupSubject = config?.group_subject || 'process_flow_group'; const groupSubject = config?.group_subject || 'process_flow_group';
const [groups, setGroups] = useState([]); const [active, setActive] = useState(''); const [modal, setModal] = useState(false); const [draft, setDraft] = useState({}); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [groups, setGroups] = useState([]); const [active, setActive] = useState(''); const [modal, setModal] = useState(false); const [draft, setDraft] = useState({}); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deleteError, setDeleteError] = useState(''); const [error, setError] = useState('');
const { group, items, loading, error: groupError, reload } = useSubjectRecords(subjectName, active); const { group, items, loading, error: groupError, reload } = useSubjectRecords(subjectName, active);
const loadGroups = async () => { try { const result = await loadRecords(groupSubject, 0, 100); const next = result?.records || []; setGroups(next); setActive((current) => current || next[0]?.name || ''); } catch (requestError) { setError(requestError?.message || '流程分组加载失败'); } }; const loadGroups = async () => { try { const result = await loadRecords(groupSubject, 0, 100); const next = result?.records || []; setGroups(next); setActive((current) => current || next[0]?.name || ''); } catch (requestError) { setError(requestError?.message || '流程分组加载失败'); } };
useEffect(() => { loadGroups(); }, [groupSubject]); useEffect(() => { loadGroups(); }, [groupSubject]);
const save = async () => { setSaving(true); setError(''); try { if (draft.group_name !== undefined) await createRecord({ subject: groupSubject, title: draft.group_name, metadata: { group_name: draft.group_name, process_list: [] } }); else if (draft.editName) await updateRecord(draft.editName, { title: draft.title, metadata: { remark: draft.remark, source: draft.source || null } }); else { const created = await createRecord({ subject: subjectName, title: draft.title, metadata: { remark: draft.remark, source: draft.source || null } }); await updateRecord(group.name, { metadata: { ...group.metadata, process_list: [...(group.metadata?.process_list || []).map(valueOf), created._id] } }); } setModal(false); setDraft({}); await loadGroups(); await reload(); } catch (requestError) { setError(requestError?.message || '保存失败'); } finally { setSaving(false); } }; const save = async () => { setSaving(true); setError(''); try { if (draft.group_name !== undefined) await createRecord({ subject: groupSubject, title: draft.group_name, metadata: { group_name: draft.group_name, process_list: [] } }); else if (draft.editName) await updateRecord(draft.editName, { title: draft.title, metadata: { remark: draft.remark, source: draft.source || null } }); else { const created = await createRecord({ subject: subjectName, title: draft.title, metadata: { remark: draft.remark, source: draft.source || null } }); await updateRecord(group.name, { metadata: { ...group.metadata, process_list: [...(group.metadata?.process_list || []).map(valueOf), created._id] } }); } setModal(false); setDraft({}); await loadGroups(); await reload(); } catch (requestError) { setError(requestError?.message || '保存失败'); } finally { setSaving(false); } };
const deleteProcess = async (item) => { if (!group || !window.confirm('确定删除该流程吗?')) return; await removeRecord(item.name || item._id); await updateRecord(group.name, { metadata: { ...group.metadata, process_list: (group.metadata?.process_list || []).map(valueOf).filter((id) => id !== (item._id || item.name)) } }); await reload(); }; const confirmDeleteProcess = async () => {
return <div className="rc-page-stack"><h2 className="mb-4 text-xl font-semibold">流程管理</h2>{error || groupError ? <Alert status="danger"><Alert.Content><Alert.Description>{error || groupError}</Alert.Description></Alert.Content></Alert> : null}<GroupTabs groups={groups} active={active} onChange={setActive} onCreate={() => { setDraft({ group_name: '' }); setModal(true); }} label="流程分组" /><Card><Card.Header><Card.Title className="mr-auto">{group?.title || '请选择流程分组'}</Card.Title>{group ? <Button variant="primary" onPress={() => { setDraft({}); setModal(true); }}>新增流程</Button> : null}</Card.Header><Card.Content>{loading ? <div className="flex min-h-32 items-center justify-center"><Spinner /></div> : !items.length ? <div className="py-16 text-center text-sm text-muted">暂无流程</div> : <ol className="space-y-3">{items.map((item, index) => <li className="flex items-center gap-3 rounded-lg border border-default p-3" key={item.name || item._id}><span className="grid h-8 w-8 place-items-center rounded-full bg-accent/10 text-sm font-semibold text-accent">{index + 1}</span><div className="min-w-0 flex-1"><div className="truncate font-medium">{item.title || item.name}</div><div className="text-sm text-muted">{item.metadata?.remark || '暂无说明'}</div></div><Button size="sm" variant="secondary" onPress={() => { setDraft({ ...item.metadata, title: item.title, editName: item.name }); setModal(true); }}>编辑</Button><Button size="sm" variant="secondary" onPress={() => history.push(`/${subjectName}/${item.name || item._id}`)}>查看</Button><Button size="sm" variant="danger" onPress={() => deleteProcess(item)}>删除</Button></li>)}</ol>}</Card.Content></Card><XinyuanModal isOpen={modal} onOpenChange={setModal} title={draft.group_name !== undefined ? '新建流程分组' : draft.editName ? '编辑流程' : '新建流程'} footer={<><Button variant="secondary" onPress={() => setModal(false)}>取消</Button><Button variant="primary" isPending={saving} onPress={save}>保存</Button></>}><div className="grid gap-4">{draft.group_name !== undefined ? <Input label="分组名称" value={draft.group_name} onChange={(event) => setDraft({ ...draft, group_name: event.target.value })} /> : <><Input label="流程名称" value={draft.title || ''} onChange={(event) => setDraft({ ...draft, title: event.target.value })} /><TextArea label="流程说明" value={draft.remark || ''} onChange={(event) => setDraft({ ...draft, remark: event.target.value })} /></>}</div></XinyuanModal></div>; if (!group || !deleteTarget) return;
setDeleting(true); setDeleteError('');
try {
await removeRecord(deleteTarget.name || deleteTarget._id);
await updateRecord(group.name, { metadata: { ...group.metadata, process_list: (group.metadata?.process_list || []).map(valueOf).filter((id) => id !== (deleteTarget._id || deleteTarget.name)) } });
setDeleteTarget(null);
await reload();
} catch (requestError) { setDeleteError(requestError?.message || '删除失败'); }
finally { setDeleting(false); }
};
return <div className="rc-page-stack"><h2 className="mb-4 text-xl font-semibold">流程管理</h2>{error || groupError ? <Alert status="danger"><Alert.Content><Alert.Description>{error || groupError}</Alert.Description></Alert.Content></Alert> : null}<GroupTabs groups={groups} active={active} onChange={setActive} onCreate={() => { setDraft({ group_name: '' }); setModal(true); }} label="流程分组" /><Card><Card.Header><Card.Title className="mr-auto">{group?.title || '请选择流程分组'}</Card.Title>{group ? <Button variant="primary" onPress={() => { setDraft({}); setModal(true); }}>新增流程</Button> : null}</Card.Header><Card.Content>{loading ? <div className="flex min-h-32 items-center justify-center"><Spinner /></div> : !items.length ? <div className="py-16 text-center text-sm text-muted">暂无流程</div> : <ol className="space-y-3">{items.map((item, index) => <li className="flex items-center gap-3 rounded-lg border border-default p-3" key={item.name || item._id}><span className="grid h-8 w-8 place-items-center rounded-full bg-accent/10 text-sm font-semibold text-accent">{index + 1}</span><div className="min-w-0 flex-1"><div className="truncate font-medium">{item.title || item.name}</div><div className="text-sm text-muted">{item.metadata?.remark || '暂无说明'}</div></div><Button size="sm" variant="secondary" onPress={() => { setDraft({ ...item.metadata, title: item.title, editName: item.name }); setModal(true); }}>编辑</Button><Button size="sm" variant="secondary" onPress={() => history.push(`/${subjectName}/${item.name || item._id}`)}>查看</Button><Button size="sm" variant="danger" onPress={() => { setDeleteError(''); setDeleteTarget(item); }}>删除</Button></li>)}</ol>}</Card.Content></Card><XinyuanModal isOpen={Boolean(deleteTarget)} onOpenChange={(open) => { if (!open && !deleting) setDeleteTarget(null); }} title="删除流程" footer={<><Button variant="secondary" isDisabled={deleting} onPress={() => setDeleteTarget(null)}>取消</Button><Button variant="danger" isPending={deleting} onPress={confirmDeleteProcess}>确认删除</Button></>}><p>确定删除“{titleOf(deleteTarget)}”吗?</p>{deleteError ? <p className="mt-3 text-danger" role="alert">{deleteError}</p> : null}</XinyuanModal><XinyuanModal isOpen={modal} onOpenChange={setModal} title={draft.group_name !== undefined ? '新建流程分组' : draft.editName ? '编辑流程' : '新建流程'} footer={<><Button variant="secondary" onPress={() => setModal(false)}>取消</Button><Button variant="primary" isPending={saving} onPress={save}>保存</Button></>}><div className="grid gap-4">{draft.group_name !== undefined ? <Input label="分组名称" value={draft.group_name} onChange={(event) => setDraft({ ...draft, group_name: event.target.value })} /> : <><Input label="流程名称" value={draft.title || ''} onChange={(event) => setDraft({ ...draft, title: event.target.value })} /><TextArea label="流程说明" value={draft.remark || ''} onChange={(event) => setDraft({ ...draft, remark: event.target.value })} /></>}</div></XinyuanModal></div>;
} }
export default function ConfiguredSpecialViews({ view, subjectName, config }) { export default function ConfiguredSpecialViews({ view, subjectName, config }) {
......
...@@ -202,6 +202,9 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) { ...@@ -202,6 +202,9 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
const value = record?.metadata?.[field]; const value = record?.metadata?.[field];
const attachments = Array.isArray(value) ? value : value ? [value] : []; const attachments = Array.isArray(value) ? value : value ? [value] : [];
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [removing, setRemoving] = useState(false);
const [removeTarget, setRemoveTarget] = useState(null);
const [removeError, setRemoveError] = useState('');
const uploadFiles = async (files) => { const uploadFiles = async (files) => {
if (!field || !files?.length) return; if (!field || !files?.length) return;
...@@ -217,10 +220,19 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) { ...@@ -217,10 +220,19 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
} }
}; };
const removeAttachment = async (attachment) => { const confirmRemoveAttachment = async () => {
if (!field || !window.confirm('确定移除这个附件吗?')) return; if (!field || !removeTarget) return;
const key = attachment?._id || attachment?.url || attachment?.name || attachment?.title; const key = removeTarget?._id || removeTarget?.url || removeTarget?.name || removeTarget?.title;
setRemoving(true);
setRemoveError('');
try {
await onPatchMetadata(field, attachments.filter((item) => (item?._id || item?.url || item?.name || item?.title) !== key)); await onPatchMetadata(field, attachments.filter((item) => (item?._id || item?.url || item?.name || item?.title) !== key));
setRemoveTarget(null);
} catch (error) {
setRemoveError(error?.message || '附件移除失败');
} finally {
setRemoving(false);
}
}; };
return ( return (
...@@ -235,7 +247,7 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) { ...@@ -235,7 +247,7 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
<a href={item?.url || '#'} rel="noreferrer" target="_blank"> <a href={item?.url || '#'} rel="noreferrer" target="_blank">
{item?.title || item?.name || `附件 ${index + 1}`} {item?.title || item?.name || `附件 ${index + 1}`}
</a> </a>
<Button isIconOnly aria-label="移除附件" size="sm" variant="tertiary" onPress={() => removeAttachment(item)}> <Button isIconOnly aria-label="移除附件" size="sm" variant="tertiary" onPress={() => { setRemoveError(''); setRemoveTarget(item); }}>
<Close size={14} /> <Close size={14} />
</Button> </Button>
</div> </div>
...@@ -251,6 +263,21 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) { ...@@ -251,6 +263,21 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
onFilesChange={uploadFiles} onFilesChange={uploadFiles}
/> />
</Card.Content> </Card.Content>
<XinyuanModal
footer={(
<>
<Button isDisabled={removing} variant="secondary" onPress={() => setRemoveTarget(null)}>取消</Button>
<Button isPending={removing} variant="danger" onPress={confirmRemoveAttachment}>确认移除</Button>
</>
)}
isOpen={Boolean(removeTarget)}
onOpenChange={(open) => { if (!open && !removing) setRemoveTarget(null); }}
size="sm"
title="移除附件"
>
<p>确定移除“{removeTarget?.title || removeTarget?.name || removeTarget?.url || '该附件'}”吗?</p>
{removeError ? <p className="mt-3 text-danger" role="alert">{removeError}</p> : null}
</XinyuanModal>
</Card> </Card>
); );
} }
...@@ -339,6 +366,8 @@ function LawRecordDetail() { ...@@ -339,6 +366,8 @@ function LawRecordDetail() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [deletingRecord, setDeletingRecord] = useState(false);
const [deleteRecordOpen, setDeleteRecordOpen] = useState(false);
const [historyItems, setHistoryItems] = useState([]); const [historyItems, setHistoryItems] = useState([]);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [collections, setCollections] = useState(() => loadStorageArray(COLLECTION_STORAGE_KEY)); const [collections, setCollections] = useState(() => loadStorageArray(COLLECTION_STORAGE_KEY));
...@@ -569,9 +598,10 @@ function LawRecordDetail() { ...@@ -569,9 +598,10 @@ function LawRecordDetail() {
}; };
const deleteRecord = async () => { const deleteRecord = async () => {
if (!recordName || !window.confirm('确定删除这条记录吗?')) return; if (!recordName) return;
try { try {
await removeRecord(recordName); await removeRecord(recordName);
setDeleteRecordOpen(false);
history.push(`/${subjectName}`); history.push(`/${subjectName}`);
} catch (requestError) { } catch (requestError) {
setError(requestError?.message || '删除失败'); setError(requestError?.message || '删除失败');
...@@ -614,7 +644,7 @@ function LawRecordDetail() { ...@@ -614,7 +644,7 @@ function LawRecordDetail() {
<TableActionButton icon={isCollected ? StarSolid : Star} label={isCollected ? '取消收藏' : '收藏'} onPress={toggleCollection} /> <TableActionButton icon={isCollected ? StarSolid : Star} label={isCollected ? '取消收藏' : '收藏'} onPress={toggleCollection} />
<TableActionButton icon={ArrowDownTrayIcon} isDisabled={!firstFile} label="下载" onPress={() => firstFile?.url && window.open(firstFile.url, '_blank', 'noopener,noreferrer')} /> <TableActionButton icon={ArrowDownTrayIcon} isDisabled={!firstFile} label="下载" onPress={() => firstFile?.url && window.open(firstFile.url, '_blank', 'noopener,noreferrer')} />
<TableActionButton icon={Edit} label="编辑" onPress={openEditModal} /> <TableActionButton icon={Edit} label="编辑" onPress={openEditModal} />
<TableActionButton icon={Delete} label="删除" tone="delete" onPress={deleteRecord} /> <TableActionButton icon={Delete} label="删除" tone="delete" onPress={() => { setError(''); setDeleteRecordOpen(true); }} />
</TableActionGroup> </TableActionGroup>
) : null} ) : null}
</div> </div>
...@@ -790,6 +820,22 @@ function LawRecordDetail() { ...@@ -790,6 +820,22 @@ function LawRecordDetail() {
</div> </div>
</section> </section>
</XinyuanModal> </XinyuanModal>
<XinyuanModal
footer={(
<>
<Button isDisabled={deletingRecord} variant="secondary" onPress={() => setDeleteRecordOpen(false)}>取消</Button>
<Button isPending={deletingRecord} variant="danger" onPress={deleteRecord}>确认删除</Button>
</>
)}
isOpen={deleteRecordOpen}
onOpenChange={(open) => { if (!open && !deletingRecord) setDeleteRecordOpen(false); }}
size="sm"
title="删除记录"
>
<p>确定删除“{record?.title || record?.name}”吗?</p>
{error ? <p className="mt-3 text-danger" role="alert">{error}</p> : null}
</XinyuanModal>
</div> </div>
); );
} }
...@@ -1302,23 +1302,9 @@ function SearchTable({ records, fields, subjectName, onEdit, onRemove }) { ...@@ -1302,23 +1302,9 @@ function SearchTable({ records, fields, subjectName, onEdit, onRemove }) {
/>; />;
} }
function collectExpandableTreeKeys(nodes = []) {
return nodes.flatMap((node) => node.children?.length ? [node.key, ...collectExpandableTreeKeys(node.children)] : []);
}
function FilterTreeCard({ title, nodes, selectedKeys, onSelect }) { function FilterTreeCard({ title, nodes, selectedKeys, onSelect }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(true);
const [expandedKeys, setExpandedKeys] = useState(() => collectExpandableTreeKeys(nodes)); const [expandedKeys, setExpandedKeys] = useState([]);
useEffect(() => {
const availableKeys = new Set(collectExpandableTreeKeys(nodes));
setExpandedKeys((current) => {
const retained = current.filter((key) => availableKeys.has(key));
const added = [...availableKeys].filter((key) => !current.includes(key));
const next = [...retained, ...added];
return next.length === current.length && next.every((key, index) => key === current[index]) ? current : next;
});
}, [nodes]);
return ( return (
<section className="law-filter-tree-card"> <section className="law-filter-tree-card">
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment