diff --git a/components/ProcessesViewer.tsx b/components/ProcessesViewer.tsx new file mode 100644 index 0000000..f3b996b --- /dev/null +++ b/components/ProcessesViewer.tsx @@ -0,0 +1,325 @@ +import { useState, memo } from 'react'; +import { isFileBlob, type FileBlob } from '@/lib/4nk/models/Data'; +import { iframeUrl } from "@/app/page"; +import MessageBus from '@/lib/4nk/MessageBus'; + +interface BlockState { + commited_in: string; + state_id: string; + pcd_commitment: Record; + public_data: Record; +} + +interface Block { + states: BlockState[]; +} + +interface Processes { + [key: string]: Block; +} + +interface ProcessesViewerProps { + processes: Processes | null; + myProcesses: string[]; + onProcessesUpdate?: (processes: Processes) => void; +} + +const compareStates = ( + currentState: BlockState, + index: number, + previousState?: BlockState, + currentPrivateData?: Record, + previousPrivateData?: Record +) => { + const result: Record = {}; + + Object.keys(currentState.public_data).forEach(key => { + const currentValue = currentState.public_data[key]; + const previousValue = previousState?.public_data[key]; + const isModified = index > 0 && previousValue !== undefined && JSON.stringify(currentValue) !== JSON.stringify(previousValue); + + result[key] = { + value: currentValue, + status: isModified ? 'modified' : 'unchanged', + hash: currentState.pcd_commitment[key], + isPrivate: false, + stateId: currentState.state_id + }; + }); + + if (index === 0 && currentPrivateData) { + Object.entries(currentPrivateData).forEach(([key, value]) => { + result[key] = { + value, + status: 'unchanged', + hash: currentState.pcd_commitment[key], + isPrivate: true, + stateId: currentState.state_id + }; + }); + } else if (previousPrivateData) { + Object.entries(previousPrivateData).forEach(([key, value]) => { + result[key] = { + value, + status: 'unchanged', + hash: previousState?.pcd_commitment[key], + isPrivate: true, + stateId: previousState!.state_id + }; + }); + if (currentPrivateData) { + Object.entries(currentPrivateData).forEach(([key, value]) => { + result[key] = { + value, + status: 'modified', + hash: currentState.pcd_commitment[key], + isPrivate: true, + stateId: currentState.state_id + }; + }); + } + } + + return result; +}; + +function ProcessesViewer({ processes, myProcesses, onProcessesUpdate }: ProcessesViewerProps) { + const [expandedBlocks, setExpandedBlocks] = useState([]); + const [isFiltered, setIsFiltered] = useState(false); + const [privateData, setPrivateData] = useState>>({}); + const [editingField, setEditingField] = useState<{processId: string; stateId: string; key: string; value: any;} | null>(null); + const [tempValue, setTempValue] = useState(null); + + const toggleBlock = (blockId: string) => { + setExpandedBlocks(prev => prev.includes(blockId) ? prev.filter(id => id !== blockId) : [...prev, blockId]); + }; + + const handleFilterClick = () => setIsFiltered(prev => !prev); + + if (!processes || Object.keys(processes).length === 0) { + return ( +
+

Aucun processus disponible

+

Connectez-vous pour voir vos processus

+
+ ); + } + + const fetchPrivateData = async (processId: string, stateId: string) => { + if (!expandedBlocks.includes(processId) || !myProcesses.includes(processId)) return; + try { + const messageBus = MessageBus.getInstance(iframeUrl); + await messageBus.isReady(); + const data = await messageBus.getData(processId, stateId); + setPrivateData(prev => ({ ...prev, [stateId]: data })); + } catch (err) { + console.error(err); + } + }; + + const handleDownload = (name: string | undefined, fileBlob: FileBlob) => { + const blob = new Blob([fileBlob.data], { type: fileBlob.type }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = name || 'download'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const formatValue = (key: string, value: string | number[] | FileBlob) => { + if (isFileBlob(value)) { + return ( + + ); + } + return {JSON.stringify(value || '')}; + }; + + const getDataIcon = (value: any) => { + if (isFileBlob(value)) return '📄'; + if (typeof value === 'string') return '📝'; + if (typeof value === 'number') return '🔢'; + if (Array.isArray(value)) return '📋'; + if (typeof value === 'boolean') return '✅'; + return '📦'; + }; + + const handleFieldUpdate = async (processId: string, stateId: string, key: string, value: any) => { + try { + const messageBus = MessageBus.getInstance(iframeUrl); + await messageBus.isReady(); + const updatedProcess = await messageBus.updateProcess(processId, stateId, { [key]: value }, [], null); + if (!updatedProcess) throw new Error('No updated process found'); + const newStateId = updatedProcess.diffs[0]?.state_id; + if (!newStateId) throw new Error('No new state id found'); + await messageBus.notifyProcessUpdate(processId, newStateId); + await messageBus.validateState(processId, newStateId); + const updatedProcesses = await messageBus.getProcesses(); + onProcessesUpdate?.(updatedProcesses); + } catch (err) { + console.error(err); + } + }; + + const renderEditForm = (key: string, value: any, onSave: (v: any) => void, onCancel: () => void) => { + if (tempValue === null) setTempValue(value); + + const handleFormClick = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); }; + + if (isFileBlob(value)) { + return ( +
+ { + e.stopPropagation(); + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + if (event.target?.result) { + setTempValue({ type: file.type, data: new Uint8Array(event.target.result as ArrayBuffer) }); + } + }; + reader.readAsArrayBuffer(file); + } + }}/> +
+ + +
+
+ ); + } + + if (typeof value === 'boolean') { + return ( +
+ +
+ + +
+
+ ); + } + + if (Array.isArray(value)) { + return ( +
+