Skip to content

Commit

Permalink
feat(SPV-755): add contact views (#915)
Browse files Browse the repository at this point in the history
  • Loading branch information
pawellewandowski98 authored May 22, 2024
1 parent 668ae1a commit a2dacbd
Show file tree
Hide file tree
Showing 10 changed files with 500 additions and 5 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"proxy": "http://localhost:3003",
"dependencies": {
"@4chain-ag/react-configuration": "^1.0.4",
"@bsv/spv-wallet-js-client": "1.0.0-beta.5",
"@bsv/spv-wallet-js-client": "1.0.0-beta.6",
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
Expand Down
113 changes: 113 additions & 0 deletions src/components/contacts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Button, Table, TableBody, TableCell, TableHead, TableRow } from '@mui/material';
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useState } from 'react';
import { JsonView } from './json-view';
import { useUser } from '../hooks/useUser';
import logger from '../logger';

const EventButton = ({ contact, event, handleContactEvent, admin }) => {
if (handleContactEvent) {
let method = `${event}Contact`;
let param = contact.paymail;
if (admin) {
method = `Admin${event}Contact`;
param = contact.id;
}
return <Button onClick={() => handleContactEvent(param, method, `${event} contact?`)}>{event} contact</Button>;
}
};

export const ContactsList = ({ items, refetch }) => {
const { spvWalletClient, admin } = useUser();
const [selectedContacts, setSelectedContacts] = useState([]);

const handleContactEvent = function (param, method, msg) {
// eslint-disable-next-line no-restricted-globals
if (confirm(msg)) {
spvWalletClient[`${method}`](param)
.then((r) => {
logger.info(`Operation performed successfully`);
refetch();
})
.catch((e) => {
logger.error(e);
alert('ERROR: Could not perform operation ' + e.message);
});
}
};

return (
<Table>
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>Full name</TableCell>
<TableCell>Paymail</TableCell>
<TableCell>Status</TableCell>
<TableCell>Created</TableCell>
<TableCell>Reject</TableCell>
<TableCell>Accept</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((contact) => (
<>
<TableRow
hover
key={`contact_${contact.id}`}
selected={selectedContacts.indexOf(contact.id) !== -1}
style={{
opacity: contact.deleted_at ? 0.5 : 1,
}}
onClick={() => {
if (selectedContacts.indexOf(contact.id) !== -1) {
setSelectedContacts([]);
} else {
setSelectedContacts([contact.id]);
}
}}
>
<TableCell>{contact.id}</TableCell>
<TableCell>{contact.fullName}</TableCell>
<TableCell>{contact.paymail}</TableCell>
<TableCell>{contact.status}</TableCell>
<TableCell>{new Date(contact.created_at).toLocaleString()}</TableCell>
<TableCell>
{contact.deleted_at ? (
<span title={`Rejected at ${contact.deleted_at}`}>Already rejected</span>
) : contact.status !== 'awaiting' ? (
<span title={`Status have to be awaiting to perform this operation`}>Wrong status</span>
) : (
<EventButton contact={contact} event="Reject" handleContactEvent={handleContactEvent} admin={admin} />
)}
</TableCell>

<TableCell>
{contact.status !== 'awaiting' ? (
contact.status === 'unconfirmed' ? (
<span>Already accepted</span>
) : (
<span title={`Status have to be awaiting to perform this operation`}>Wrong status</span>
)
) : (
<EventButton contact={contact} event="Accept" handleContactEvent={handleContactEvent} admin={admin} />
)}
</TableCell>
</TableRow>
{selectedContacts.indexOf(contact.id) !== -1 && (
<TableRow>
<TableCell colSpan={5}>
<JsonView jsonData={contact} />
</TableCell>
</TableRow>
)}
</>
))}
</TableBody>
</Table>
);
};

ContactsList.propTypes = {
items: PropTypes.array.isRequired,
};
16 changes: 16 additions & 0 deletions src/components/dashboard-sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ViewListIcon from '@mui/icons-material/ViewList';
import MonetizationOnIcon from '@mui/icons-material/MonetizationOn';
import AddIcon from '@mui/icons-material/Add';
import KeyIcon from '@mui/icons-material/Key';
import ContactsIcon from '@mui/icons-material/Contacts';
import BitcoinIcon from '@mui/icons-material/CurrencyBitcoin';
import PaymailIcon from '@mui/icons-material/Message';
import { CredTypeAdmin, useCredentials } from '../hooks/useCredentials';
Expand Down Expand Up @@ -59,6 +60,11 @@ const adminItems = [
icon: <BitcoinIcon fontSize="small" />,
title: 'XPubs',
},
{
href: '/admin/contacts',
icon: <ContactsIcon fontSize="small" />,
title: 'Contacts',
},
];

const items = [
Expand Down Expand Up @@ -102,6 +108,16 @@ const items = [
icon: <KeyIcon fontSize="small" />,
title: 'Access Keys',
},
{
href: '/contacts',
icon: <ContactsIcon fontSize="small" />,
title: 'Contacts',
},
{
href: '/contacts-new',
icon: <AddIcon fontSize="small" />,
title: 'New Contact',
},
];

export const DashboardSidebar = (props) => {
Expand Down
98 changes: 98 additions & 0 deletions src/components/listing/newAdmin.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useNavigate } from 'react-router-dom';
import PerfectScrollbar from 'react-perfect-scrollbar';

import { Alert, Box, Card, TextField, Typography } from '@mui/material';

import { useNewQueryList } from '../../hooks/useNewQueryList';
import { useDebounce } from '../../hooks/useDebounce';
import { useUser } from '../../hooks/useUser';

// TODO: refactor this name after adjusting every view to new search endpoints
export const NewAdminListing = function ({
modelFunction,
title,
ListingComponent,
conditions,
filter: initialFilter,
setFilter,
additionalFilters,
}) {
const navigate = useNavigate();

const [searchFilter, setSearchFilter] = useState('');
const debouncedFilter = useDebounce(searchFilter, 500);

const { admin } = useUser();
const { items, loading, error, Pagination, setRefreshData } = useNewQueryList({
modelFunction,
conditions,
});

useEffect(() => {
if (!admin) {
navigate('/');
}
}, [navigate, admin]);

useEffect(() => {
if (initialFilter) {
setSearchFilter(initialFilter);
}
}, [initialFilter]);

useEffect(() => {
if (setFilter) {
setFilter(debouncedFilter);
}
}, [setFilter, debouncedFilter]);

return (
<>
<Box display="flex" flexDirection="row" alignItems="center">
<Typography color="inherit" variant="h4">
{title}
</Typography>
<Box display="flex" flex={1} flexDirection="row" alignItems="center">
<TextField
fullWidth
label="Filter"
margin="normal"
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
type="text"
variant="outlined"
style={{
marginLeft: 20,
}}
/>
{additionalFilters ? additionalFilters() : ''}
</Box>
</Box>
{loading ? (
<>Loading...</>
) : (
<>
{!!error && <Alert severity="error">{error}</Alert>}
<Card>
<PerfectScrollbar>
<ListingComponent items={items} refetch={() => setRefreshData(+new Date())} />
</PerfectScrollbar>
<Pagination />
</Card>
</>
)}
</>
);
};

NewAdminListing.propTypes = {
ListingComponent: PropTypes.func.isRequired,
modelFunction: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
conditions: PropTypes.object,
filter: PropTypes.string,
setFilter: PropTypes.func,
additionalFilters: PropTypes.func,
};
82 changes: 82 additions & 0 deletions src/hooks/useNewQueryList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useUser } from './useUser';
import React, { useEffect, useState } from 'react';
import { TablePagination } from '@mui/material';
import logger from '../logger';

// TODO: refactor this name after adjusting every view to new search endpoints
export const useNewQueryList = function ({ modelFunction, conditions }) {
const { spvWalletClient } = useUser();

const [items, setItems] = useState([]);
const [itemsCount, setItemsCount] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [refreshData, setRefreshData] = useState(0);

const [limit, setLimit] = useState(10);
const [page, setPage] = useState(0);

const handleLimitChange = (event) => {
setLimit(event.target.value);
};

const handlePageChange = (event, newPage) => {
setPage(newPage);
};

useEffect(() => {
setPage(0);
}, [limit]);

useEffect(() => {
setLoading(true);
const queryParams = {
page: page + 1,
page_size: limit,
order_by_field: 'created_at',
sort_direction: 'desc',
};
if (!spvWalletClient) {
return;
}
spvWalletClient[`${modelFunction}`](conditions || {}, {}, queryParams)
.then((response) => {
setItems([...response.content]);
setItemsCount(response.page.totalElements);
setError('');
setLoading(false);
})
.catch((e) => {
logger.error(e);
setError(e.message);
setLoading(false);
});
}, [refreshData, conditions, page, limit]);

const Pagination = () => {
return (
<TablePagination
component="div"
count={itemsCount}
onPageChange={handlePageChange}
onRowsPerPageChange={handleLimitChange}
page={page}
rowsPerPage={limit}
rowsPerPageOptions={[5, 10, 25, 50, 100]}
showFirstButton={true}
showLastButton={true}
/>
);
};

return {
items,
loading,
error,
setError,
Pagination,
refreshData,
setRefreshData,
spvWalletClient,
};
};
47 changes: 47 additions & 0 deletions src/pages/admin/contacts.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { useEffect, useState } from 'react';

import { DashboardLayout } from '../../components/dashboard-layout';
import { Box } from '@mui/material';
import { ContactsList } from '../../components/contacts';
import { NewAdminListing } from '../../components/listing/newAdmin';

export const AdminContacts = () => {
const [filter, setFilter] = useState('');
const [showRejected, setShowRejected] = useState(false);
const [conditions, setConditions] = useState(null);

// TODO: SPV-699 - refactor this to filter by more parameters
useEffect(() => {
const conditions = {};
if (filter) {
conditions.paymail = filter;
}

conditions.include_deleted = showRejected;

setConditions(conditions);
}, [filter, showRejected]);

const additionalFilters = function () {
return (
<Box style={{ marginLeft: 20 }}>
Show Rejected <input type="checkbox" checked={showRejected} onClick={() => setShowRejected(!showRejected)} />
</Box>
);
};

return (
<DashboardLayout>
<NewAdminListing
key="admin_contacts_listing"
modelFunction="AdminGetContacts"
title="Contacts"
ListingComponent={ContactsList}
filter={filter}
setFilter={setFilter}
conditions={conditions}
additionalFilters={additionalFilters}
/>
</DashboardLayout>
);
};
Loading

0 comments on commit a2dacbd

Please sign in to comment.