Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions bundles/processing/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as React from 'react';
import { useQuery } from 'react-query';
import { Route, Switch } from 'react-router';
import { Accent, AppContainer, Theme } from '@spyrothon/sparx';

import { useConstants } from '@common/Constants';
import { usePermission } from '@public/api/helpers/auth';
import APIClient from '@public/apiv2/APIClient';
import { setAPIRoot } from '@public/apiv2/HTTPUtils';
import { ProcessingSocket } from '@public/apiv2/sockets/ProcessingSocket';

import { loadMe } from './modules/auth/AuthStore';
import { loadDonations } from './modules/donations/DonationsStore';
import useProcessingStore from './modules/processing/ProcessingStore';
import { loadProcessActions } from './modules/processing/ProcessActionsStore';
import * as Theming from './modules/theming/Theming';
import ProcessDonations from './pages/ProcessDonations';
import ReadDonations from './pages/ReadDonations';
Expand All @@ -18,7 +21,6 @@ import '@spyrothon/sparx/style.css';

export default function App() {
const canChangeDonations = usePermission('tracker.change_donation');
const { processDonation } = useProcessingStore();
const { theme, accent } = Theming.useThemeStore();

const { APIV2_ROOT } = useConstants();
Expand All @@ -27,23 +29,31 @@ export default function App() {
setAPIRoot(APIV2_ROOT);
}, [APIV2_ROOT]);

useQuery('auth.me', () => APIClient.getMe(), {
onSuccess: me => loadMe(me),
staleTime: 5 * 60 * 1000,
});

React.useEffect(() => {
const unsubActions = ProcessingSocket.on('processing_action', event => {
loadDonations([event.donation]);
if (event.action !== 'unprocessed') {
processDonation(event.donation, event.action, false);
}
loadProcessActions([event.action]);
});

const unsubNewDonations = ProcessingSocket.on('donation_received', event => {
loadDonations([event.donation]);
});

const unsubUpdatedDonations = ProcessingSocket.on('donation_updated', event => {
loadDonations([event.donation]);
});

return () => {
unsubActions();
unsubNewDonations();
unsubUpdatedDonations();
};
}, [processDonation]);
}, []);

return (
<AppContainer theme={theme as Theme} accent={accent as Accent}>
Expand Down
2 changes: 1 addition & 1 deletion bundles/processing/modules/layout/SidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function SidebarLayout(props: SidebarLayoutProps) {

return (
<div className={styles.container}>
<Stack className={styles.sidebar} spacing="space-xl">
<Stack className={styles.sidebar} spacing="space-xl" wrap={false}>
<LayoutHeader event={event} subtitle={subtitle} />
{sidebar}
</Stack>
Expand Down
16 changes: 16 additions & 0 deletions bundles/processing/modules/processing/ActionHistoryModal.mod.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.container {
min-width: 560px;
width: 60vw;
height: 80vh;
overflow-y: scroll;
}

.comment {
position: relative;
overflow-wrap: anywhere;
white-space: pre-wrap;
}

.action {
width: 100%;
}
130 changes: 130 additions & 0 deletions bundles/processing/modules/processing/ActionHistoryModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as React from 'react';
import { useMutation, useQuery } from 'react-query';
import { Anchor, Button, Card, Checkbox, Header, openModal, Stack, Text, useTooltip } from '@spyrothon/sparx';

import APIClient from '@public/apiv2/APIClient';
import { DonationProcessAction } from '@public/apiv2/APITypes';
import * as CurrencyUtils from '@public/util/currency';
import Undo from '@uikit/icons/Undo';

import { AdminRoutes, useAdminRoute } from '../../Routes';
import { useMe } from '../auth/AuthStore';
import { useDonation } from '../donations/DonationsStore';
import RelativeTime from '../time/RelativeTime';
import { loadProcessActions, useOwnProcessActions } from './ProcessActionsStore';

import styles from './ActionHistoryModal.mod.css';

export function ActionHistoryModalButton() {
function handleOpen() {
openModal(() => <ActionHistoryModal />);
}

return <Button onClick={handleOpen}>View Action History</Button>;
}

function ActionEntry({ action }: { action: DonationProcessAction }) {
const storedDonation = useDonation(action.donation_id);
const donation = action.donation ?? storedDonation;
const hasDonation = donation != null;
const isUndo = action.originating_action != null;

const donationLink = useAdminRoute(AdminRoutes.DONATION(action.donation_id));
const amount = CurrencyUtils.asCurrency(donation.amount);
const timestamp = React.useMemo(() => new Date(action.occurred_at), [action.occurred_at]);

const [undoTooltipProps] = useTooltip<HTMLButtonElement>(`Reset to ${action.from_state}`);
const undo = useMutation(() => APIClient.undoProcessAction(action.id));

return (
<Card className={styles.action}>
<Stack direction="horizontal" justify="space-between" wrap={false} key={action.id}>
<div>
<Text variant="text-sm/normal">
{isUndo ? (
<em>
Undo <strong>{action.from_state}</strong> to <strong>{action.to_state}</strong>
</em>
) : (
<strong>{action.to_state}</strong>
)}
{' · '}
<RelativeTime time={timestamp} forceRelative />
</Text>
<Text variant="text-sm/normal">
<Anchor href={donationLink} newTab>
#{action.donation_id}
</Anchor>
{' · '}
{hasDonation ? (
<>
<strong>{amount}</strong> from <strong>{donation.donor_name}</strong>
</>
) : (
'Donation info not available'
)}
</Text>
{donation?.comment != null ? (
<Text className={styles.comment} variant="text-xs/normal">
{donation.comment.slice(0, 500)}
{donation.comment.length > 500 ? '...' : null}
</Text>
) : null}
</div>
{!isUndo ? (
<Button
{...undoTooltipProps}
variant="link"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => undo.mutate()}
disabled={undo.isLoading}>
<Undo />
</Button>
) : null}
</Stack>
</Card>
);
}

export default function ActionHistoryModal() {
const me = useMe();
// -1 should be an invalid user id, meaning we'll get back an empty history
// until Me has loaded.
const history = useOwnProcessActions(me?.id ?? -1);

const [showUndos, setShowUndos] = React.useState(true);

const { data: initialHistory } = useQuery(`processing.action-history`, () => APIClient.getProcessActionHistory(), {
// This data comes in over the socket as the user interacts with the page.
// This initial fetch is just to back-populate data when they open the modal,
// So it only needs to run once-ish.
cacheTime: 60 * 60 * 1000,
staleTime: 60 * 60 * 1000,
refetchOnWindowFocus: false,
});
React.useEffect(() => {
if (initialHistory == null) return;

loadProcessActions(initialHistory);
}, [initialHistory]);

return (
<Card className={styles.container}>
<Stack as={Header} direction="horizontal" justify="space-between" tag="h1" variant="header-md/normal" withMargin>
Action History
<Checkbox
checked={showUndos}
// eslint-disable-next-line react/jsx-no-bind
onChange={event => setShowUndos(event.target.checked)}
label="Show Undos"
/>
</Stack>
<Stack align="stretch">
{history.map(action => {
if (!showUndos && action.originating_action != null) return null;
return <ActionEntry key={action.id} action={action} />;
})}
</Stack>
</Card>
);
}
21 changes: 4 additions & 17 deletions bundles/processing/modules/processing/ActionLog.mod.css
Original file line number Diff line number Diff line change
@@ -1,33 +1,20 @@
.action {
display: flex;
width: 100%;
margin: 8px 0;
transform-origin: top;

&:hover,
&:focus {
background-color: var(--background-hover-alt);

& .undoButton {
display: block;
}
}
}

.info {
flex: 1 1 auto;
padding: 8px 0;
border-radius: var(--radius-normal);
}

.actionEnter {
opacity: 0;
transform: scaleY(0);
transform-origin: top;
}

.actionEnterActive {
opacity: 1;
transform: scaleY(1);
transform-origin: top;
transition: 160ms ease-out;
transition-delay: 50ms;
}

.actionExit {
Expand Down
Loading