@foreverrbum/ethsign
Version:
This package will allow you to electronically sign documents within your application
536 lines (502 loc) • 24.9 kB
JavaScript
import React, { useState, useEffect, Fragment } from 'react';
import '../styles/documents.scss';
import DocumentSidebar from './Dashboard/DocumentSidebar';
import DeleteIcon from '../assets/contracts_actions/archive.svg';
import DownloadIcon from '../assets/contracts_actions/download.svg';
import LeftArrowPage from '../assets/leftarrow_page.svg';
import RightArrowPage from '../assets/rightarrow_page.svg';
import CheckIcon from '../assets/check.svg';
import _ from 'lodash';
import Searchbox from './Dashboard/Searchbox';
import Loader from './UI/loader';
import { useIntl, FormattedMessage } from 'react-intl';
import Alert from './Alert';
import DocumentRow from './Dashboard/DocumentRow';
import { archiveDocuments, storeNotif } from '../helpers/dashboard';
const Documents = (props) => {
/*
* Statuses:
* -2: 'Search Results'
* -1: 'All Status'
* 0: 'PDF Not Uploaded'
* 1: 'More Signers Needed'
* 2: 'Pending Signatures'
* 3: 'All Signed'
* 4: 'Waiting For Others'
*/
const { ethAccount, contract, web3, filter, handleFilter, status, handleStatus, data, dataKeys, dataObj, handleDataObj, dataAdded, handleDataAdded, handleActivePage, ensEnabled, handleData, name, handleName, error, search, handleSearch, searchData, handleSearchData, loaded, handleLoaded, reloadContractDetails } = props;
const [showArchivePopup, handleShowArchivePopup] = useState(false);
const [loading, handleLoading] = useState(false);
const [archivedWaiting, handleArchivedWaiting] = useState(false);
const [displayDataEmpty, handleDisplayDataEmpty] = useState(false);
const [displayData, handleDisplayData] = useState(null);
const [displayDataSize, handleDisplayDataSize] = useState(0);
const [actualDate, handleActualDate] = useState(true);
const [mobileSidebar, handleMobileSidebar] = useState(false);
const [tableLabel, handleTableLabel] = useState("");
const [selectAll, handleSelectAll] = useState(false);
const [selected, handleSelected] = useState([]);
const [selectedUpdated, handleSelectedUpdated] = useState(false); // we will just toggle this every time we update selected
const [update, handleUpdate] = useState(true);
const [active, handleActive] = useState(null);
const [pageNumber, handlePageNumber] = useState(0);
const [contractsPerPage, handleContractsPerPage] = useState(10);
const [totalContractsInCurrentFilter, handleTotalContractsInCurrentFilter] = useState(0);
const { formatMessage } = useIntl();
useEffect(() => {
let isSubscribed = true;
handleActivePage('contracts');
return () => isSubscribed = false;
}, []);
useEffect(() => {
handleActive(null);
handleDisplayDataEmpty(false);
handleDisplayData(null);
handleDisplayDataSize(0);
handleStatus(-1);
}, [ethAccount, contract, filter]);
useEffect(() => {
handlePageNumber(0);
}, [filter])
useEffect(() => {
let dd;
switch (filter) {
case 'original':
handleTableLabel('CREATED_BY_ME');
dd = dataKeys?.mine;
break;
case 'shared':
handleTableLabel('SHARED_WITH_ME');
dd = dataKeys?.shared;
break;
case 'all':
handleTableLabel('RECENT_FILES');
dd = dataKeys?.all;
break;
case 'pending':
handleTableLabel('ACTION_REQUIRED');
dd = dataKeys?.pending;
break;
case 'voted':
handleTableLabel('WAITING_FOR_OTHERS');
dd = dataKeys?.voted;
break;
case 'consensus':
handleTableLabel('ALL_SIGNED');
dd = dataKeys?.consensus;
break;
case 'expiring':
handleTableLabel('EXPIRING_SOON');
dd = dataKeys?.expiring;
break;
case 'archived':
handleTableLabel('ARCHIVED_CONTRACTS');
dd = dataKeys?.archived;
break;
case 'search':
handleTableLabel('SEARCH_RESULTS');
dd = searchData;
break;
default:
handleTableLabel('CREATED_BY_ME');
dd = dataKeys?.mine;
}
if(!dataKeys || !dataObj) {
return;
}
// Manage status filter
let finalDd = [];
if(status !== -1) {
for(let docKey of dd) {
if(status === dataObj[docKey]?.status) {
finalDd.push(docKey);
}
}
} else {
finalDd = dd;
}
let pageDd = finalDd.slice((pageNumber * contractsPerPage), ((pageNumber + 1) * contractsPerPage));
handleSelectAll(false);
handleSelected([]);
handleDisplayData(pageDd);
handleDisplayDataEmpty(pageDd == null || pageDd.length == 0);
handleUpdate(!update);
handleDisplayDataSize(_.size(pageDd));
handleTotalContractsInCurrentFilter(finalDd.length);
// handleLoaded(true);
}, [filter, status, name, dataKeys, dataObj, dataAdded, pageNumber, contractsPerPage]);
// useEffect(() => {
// let isSubscribed = true;
// let dd;
// // Grab list of data in the filter
// switch (filter) {
// case 'original':
// handleTableLabel('CREATED_BY_ME');
// dd = data ? data.mine : null;
// break;
// case 'shared':
// handleTableLabel('SHARED_WITH_ME');
// dd = data ? data.shared : null;
// break;
// case 'all':
// handleTableLabel('RECENT_FILES');
// dd = data ? data.all : null;
// break;
// case 'pending':
// handleTableLabel('ACTION_REQUIRED');
// dd = data ? data.pending : null;
// break;
// case 'voted':
// handleTableLabel('WAITING_FOR_OTHERS');
// dd = data ? data.voted : null;
// break;
// case 'consensus':
// handleTableLabel('ALL_SIGNED');
// dd = data ? data.consensus : null;
// break;
// case 'expiring':
// handleTableLabel('EXPIRING_SOON');
// dd = data ? data.expiring : null;
// break;
// case 'archived':
// handleTableLabel('ARCHIVED_CONTRACTS');
// dd = data ? data.archived : null;
// break;
// case 'search':
// handleTableLabel('SEARCH_RESULTS');
// dd = searchData;
// break;
// default:
// handleTableLabel('CREATED_BY_ME');
// dd = data ? data.mine : null;
// }
// // Manage status filter
// let finalDd = [];
// if(status !== -1) {
// for(let doc in dd) {
// if(status === dd[doc].status) {
// finalDd.push(dd[doc]);
// }
// }
// } else {
// finalDd = dd;
// }
// handleSelectAll(false);
// handleSelected([]);
// handleDisplayData(finalDd);
// handleDisplayDataEmpty(finalDd == null || finalDd.length == 0);
// handleUpdate(!update);
// handleDisplayDataSize(_.size(finalDd));
// // handleLoaded(true);
// return () => isSubscribed = false;
// }, [filter, status, data, dataAdded]);
useEffect(() => {
// Manage name filter
if(!dataKeys || !dataObj) {
return;
}
let finalDd = [];
if(search.length > 0) {
for(let docKey of dataKeys.mine) {
let doc = dataObj[docKey];
if(doc?.name?.toLowerCase().includes(search)) {
doc.filter = formatMessage({id: 'CREATED_BY_ME'});
finalDd.push(docKey);
}
}
for(let docKey of dataKeys.shared) {
let doc = dataObj[docKey];
if(doc?.name?.toLowerCase().includes(search)) {
doc.filter = formatMessage({id: 'SHARED_WITH_ME'});
finalDd.push(docKey);
}
}
for(let docKey of dataKeys.archived) {
let doc = dataObj[docKey];
if(doc?.name?.toLowerCase().includes(search)) {
doc.filter = formatMessage({id: 'ARCHIVED_CONTRACTS'});
finalDd.push(docKey);
}
}
}
// Grab the first 5 items of the array
handleSearchData(finalDd);
}, [search, dataKeys, dataObj, dataAdded]);
const triggerSearch = () => {
handleSelectAll(false);
handleSelected([]);
handleDisplayData(searchData);
handleDisplayDataEmpty(searchData == null || searchData.length == 0);
handleDisplayDataSize(_.size(searchData));
handleLoaded(true);
}
// This expects a boolean. True = next, false = prev
const changePage = (page) => {
if(page == true) {
if((((pageNumber + 1) * contractsPerPage) + 1) <= totalContractsInCurrentFilter) {
handlePageNumber(pageNumber + 1);
}
} else if(page == false) {
if(pageNumber > 0) {
handlePageNumber(pageNumber - 1);
}
}
}
const selectedHandler = (id, add) => {
let arr = selected;
if (add) {
arr.push(id)
} else {
_.pull(arr, id)
}
const size = _.size(arr)
if (size == displayDataSize) {
handleSelectAll(true);
}
// The array is the same memory address despite changing.
// This will not trigger an update, so use the following
// call to update the UI accordingly.
handleSelected(arr);
handleSelectedUpdated(!selectedUpdated);
}
const getEmptyTableMessage = () => {
if(error) {
return <FormattedMessage id='ERROR_LOADING_CONTRACTS'/>
}
switch (filter) {
case 'original':
return <FormattedMessage id='CONTRACTS_CREATED_BY_ME'/>
case 'shared':
return <FormattedMessage id='CONTRACTS_SHARED_WITH_ME'/>
case 'all':
return <FormattedMessage id='ALL_CONTRACTS'/>
case 'pending':
return <FormattedMessage id='CONTRACTS_WAITING_FOR_ME'/>
case 'voted':
return <FormattedMessage id='CONTRACTS_WAITING_FOR_OTHERS'/>
case 'consensus':
return <FormattedMessage id='CONTRACTS_CONSENSUS'/>
case 'expiring':
return <FormattedMessage id='CONTRACTS_EXPIRING'/>
case 'archived':
return <FormattedMessage id='CONTRACTS_ARCHIVE'/>
case 'search':
return <FormattedMessage id='CONTRACTS_SEARCH'/>
default:
return <FormattedMessage id='NO_CONTRACTS_TO_DISPLAY'/>
}
}
const handleArchiveData = async (documentKey, idx) =>{
let index = idx
let type = 3; // type for archiving
if (filter == "shared") {
type = type + 1;
} else if (filter != 'original') {
let mine_index = _.indexOf(dataKeys.mine, documentKey);
if (mine_index > -1) {
index = mine_index
} else {
index = _.indexOf(dataKeys.shared, documentKey);
type = type + 1
}
}
handleActive(null);
await handleData(documentKey, index, type)
}
const handleMultiArchiveData = async (docs) =>{
// Update UI
let tempObj = dataObj;
for(let doc of docs) {
tempObj[doc.documentKey].loading = true;
}
handleDataObj(tempObj);
handleDataAdded(!dataAdded);
docs.slice().forEach((doc) => {
doc.index = doc.idx;
if (filter == "shared") {
doc.type = doc.type + 1;
} else if (filter != 'original') {
let mine_index = _.indexOf(dataKeys.mine, doc.documentKey);
if (mine_index > -1) {
doc.index = mine_index
} else {
doc.index = _.indexOf(dataKeys.shared, doc.documentKey);
doc.type = doc.type + 1;
}
}
})
handleActive(null);
// Sort with highest doc.index first and lowest doc.index last
docs.sort((a,b) => (a.index > b.index) ? -1 : ((b.index > a.index) ? 1 : 0));
for(let doc of docs) {
await handleData(doc.documentKey, doc.index, doc.type);
}
}
const archiveSelectedDocuments = async () => {
let docs = [];
let docKeys = [];
for(let idx of selected) {
docs.push({documentKey: displayData[idx], idx: idx, type: 3});
docKeys.push(displayData[idx]);
}
handleLoading(true);
let tx = await archiveDocuments(web3, contract, docKeys, handleArchivedWaiting, formatMessage);
if (tx == true){
handleActive(null);
handleShowArchivePopup(false);
await handleMultiArchiveData(docs);
storeNotif('', formatMessage({id: docKeys.length > 1 ? 'SUCCESSFULLY_ARCHIVED_CONTRACTS' : "SUCCESSFULLY_ARCHIVED_CONTRACT"}), 'success');
// docs.slice().reverse().forEach(async (doc) => {
// handleArchiveData(doc.documentKey, doc.idx);
// })
handleSelected([]);
handleSelectAll(false);
}
handleLoading(false);
handleArchivedWaiting(false);
}
return (
<div className="flex-grow flex flex-col relative text-gray-80">
<div className="w-full flex-grow max-w-7xl mx-auto flex relative">
<DocumentSidebar filter={filter} handleFilter={(filter) => { handleSearch(""); handleSearchData([]); try{document.getElementById("search-contract-name").value = '';} catch(err) {} handleFilter(filter); }} mobileSidebar={mobileSidebar} handleMobileSidebar={handleMobileSidebar} handleActivePage={handleActivePage}/>
{contract &&
<Fragment>
<div className="flex-grow px-3 md:px-8 mt-4 sm:mt-12">
<Searchbox
contract={contract}
provider={web3}
status={status}
dataObj={dataObj}
triggerSearch={triggerSearch}
handleStatus={handleStatus}
name={search}
handleName={handleSearch}
filter={filter}
handleFilter={handleFilter}
tableLabel={tableLabel.length > 0 ? formatMessage({id: tableLabel}) : ''}
handleMobileSidebar={handleMobileSidebar}
data={searchData}
handleActivePage={handleActivePage}
pageNumber={pageNumber}
changePage={changePage}
contractsPerPage={contractsPerPage}
handleContractsPerPage={handleContractsPerPage}
totalContractsInCurrentFilter={totalContractsInCurrentFilter} />
<div className="table-container w-full">
<table className="w-full mx-auto text-left text-15">
<thead className="hidden sm:table-header-group font-bold">
<tr>
<th className="select-none h-12 min-w-72 px-3 border-t border-b border-gray-25 flex">
{/* <div className="invisible w-5 h-5 mr-3 border border-gray-300 rounded-sm flex justify-center cursor-pointer"></div> */}
<div className="select-none my-auto w-5 h-5 mr-3 border border-gray-300 rounded-sm flex justify-center cursor-pointer"
onClick={() => {
if (!selectAll) {
let arr = _.range(displayDataSize);
handleSelected(arr);
} else {
handleSelected([]);
}
handleSelectAll(!selectAll)
handleUpdate(!update);
}}
>
{selectAll && <img src={CheckIcon} />}
</div>
{selected.length > 1 ? (
<div className="select-none flex -m-px">
<img src={DeleteIcon} className="mx-3 my-2 cursor-pointer" onClick={(event) => {
event.stopPropagation();
if(filter == 'archived') {
return;
}
handleShowArchivePopup(true);
}}/>
{/* <img src={DownloadIcon} className="mx-3 my-2 cursor-pointer" /> */}
</div>
) : (
<div className="my-auto">{formatMessage({id: 'CONTRACT_NAME'})}</div>
)}
</th>
{!(selected?.length > 1) ?
<>
<th className="select-none h-12 min-w-48 px-3 border-t border-b border-gray-25 ">
<div className="my-auto">{formatMessage({id: 'STATUS'})}</div>
</th>
<th className="select-none h-12 min-w-32 px-3 border-t border-b border-gray-25 ">
<div className="my-auto">{formatMessage({id: 'ACTION'})}</div>
</th>
</>
:
<>
<th className="select-none h-12 min-w-48 px-3 border-t border-b border-gray-25"></th>
<th className="select-none h-12 min-w-32 px-3 border-t border-b border-gray-25"></th>
</>
}
</tr>
</thead>
{displayData != null && !error && <tbody>
{displayData.map((docKey, idx) => (
<>
<DocumentRow
key={idx}
idx={idx}
active={active}
handleActive={handleActive}
update={update}
doc={dataObj ? dataObj[docKey] : null}
actualDate={actualDate}
selectAll={selectAll}
handleSelectAll={handleSelectAll}
selectedHandler={selectedHandler}
contract={contract}
provider={web3}
filter={filter}
handleArchiveData={handleArchiveData}
ethAccount={ethAccount}
ensEnabled={ensEnabled}
reloadContractDetails={reloadContractDetails}
/>
</>
))}
{/* NOTE: This adds a < # > to the bottom of the contract list, showing the page number as # and left/right page switchers. I didn't like it, but leaving it here for reference.
<tr>
<td colSpan={3} className="mt-auto mx-5 border-t border-gray-25">
<div className="flex flex-row justify-center">
<img className="ml-2 h-4 w-4 my-auto cursor-pointer select-none" src={LeftArrowPage} onClick={() => changePage(false)} />
<div className="text-15 flex flex-row text-gray-300 mx-2">
{pageNumber + 1}
</div>
<img className="mr-2 h-4 w-4 my-auto cursor-pointer select-none" src={RightArrowPage} onClick={() => changePage(true)} />
</div>
</td>
</tr> */}
</tbody>}
</table>
</div>
<div className="flex justify-center">
{/* {!error && !loaded ? <Loader /> : ''} */}
</div>
{(error || (filter !== 'search' && displayDataEmpty && loaded) || (filter === 'search' && displayDataEmpty && loaded)) &&
<div className="mt-8 flex justify-center text-15 text-center text-gray-500 select-none">{getEmptyTableMessage()}</div>
}
</div>
</Fragment>
}
</div>
{showArchivePopup &&
<Alert
title={formatMessage({id: 'ARCHIVE_CONTRACTS'})}
message={formatMessage({id: 'ARE_YOU_WANT_TO_ARCHIVE_SELECTED_DOCS'})}
closeButtonText={formatMessage({id:'CANCEL'})}
loading={loading}
loadingText={archivedWaiting ? formatMessage({id: 'WAITING_FOR_CONFIRMATIONS_FROM_NEWWORK'}) : formatMessage({id: 'ARCHIVING_CONTRACTS'})}
closeCallback={() => handleShowArchivePopup(false)}
closeOnOutsideClick={true}
okButtonText={formatMessage({id:'ARCHIVE'})}
okCallback={() => {archiveSelectedDocuments();}}
/>
}
</div>
);
}
export default Documents;