Skip to content

Commit

Permalink
Merge pull request #786 from CareTogether/fix-clipboard-safari
Browse files Browse the repository at this point in the history
Fix #660
  • Loading branch information
LarsKemmann authored Oct 14, 2024
2 parents c272ffe + 2d4df16 commit aeedd68
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 31 deletions.
113 changes: 93 additions & 20 deletions src/caretogether-pwa/src/Families/ManageUserDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import {
Checkbox,
ListItemText,
useMediaQuery,
FormControl,
IconButton,
InputAdornment,
InputLabel,
OutlinedInput,
Box,
CircularProgress,
} from '@mui/material';
import { Permission, Person, UserInfo } from '../GeneratedClient';
import {
Expand All @@ -21,7 +28,12 @@ import {
import { useBackdrop } from '../Hooks/useBackdrop';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { personNameString } from './PersonName';
import { AccountCircle, NoAccounts, PersonAdd } from '@mui/icons-material';
import {
AccountCircle,
NoAccounts,
PersonAdd,
ContentCopy,
} from '@mui/icons-material';
import { organizationConfigurationQuery } from '../Model/ConfigurationModel';
import { useState } from 'react';
import { api } from '../Api/Api';
Expand All @@ -30,6 +42,7 @@ import {
visibleAggregatesState,
} from '../Model/Data';
import { UserLoginInfoDisplay } from './UserLoginInfoDisplay';
import { useGlobalSnackBar } from '../Hooks/useGlobalSnackBar';

interface ManageUserDrawerProps {
onClose: () => void;
Expand All @@ -51,18 +64,27 @@ export function ManageUserDrawer({

const withBackdrop = useBackdrop();

async function invitePersonUser() {
await withBackdrop(async () => {
const inviteLink = await api.users.generatePersonInviteLink(
organizationId,
locationId,
adult.id
);
await navigator.clipboard.writeText(inviteLink);
alert(
`The invite link for ${personNameString(adult)} has been copied to your clipboard.`
);
});
const [inviteLink, setInviteLink] = useState<string | null>(null);
const [inviteLinkLoading, setInviteLinkLoading] = useState(false);

async function generateInviteLink() {
setInviteLinkLoading(true);

const inviteLink = await api.users.generatePersonInviteLink(
organizationId,
locationId,
adult.id
);

setInviteLinkLoading(false);
setInviteLink(inviteLink);
}

const { setAndShowGlobalSnackBar } = useGlobalSnackBar();

function copyInviteLink() {
navigator.clipboard.writeText(String(inviteLink));
setAndShowGlobalSnackBar('Invite link copied!');
}

const [selectedRoles, setSelectedRoles] = useState(user?.locationRoles ?? []);
Expand Down Expand Up @@ -113,11 +135,11 @@ export function ManageUserDrawer({
currentEntry.constructor === updatedAggregate.constructor
)
? current.map((currentEntry) =>
currentEntry.id === updatedAggregate.id &&
currentEntry.id === updatedAggregate.id &&
currentEntry.constructor === updatedAggregate.constructor
? updatedAggregate
: currentEntry
)
? updatedAggregate
: currentEntry
)
: current.concat(updatedAggregate)
);
};
Expand Down Expand Up @@ -175,13 +197,64 @@ export function ManageUserDrawer({
variant="contained"
color="primary"
endIcon={<PersonAdd />}
onClick={invitePersonUser}
onClick={generateInviteLink}
disabled={inviteLinkLoading}
sx={{ marginLeft: 2 }}
>
Invite
</Button>
</p>
)}

{inviteLinkLoading && (
<Box
sx={{
mb: 3,
display: 'flex',
flexFlow: 'column',
alignItems: 'center',
opacity: 0,
animation: 'fadeIn 0.3s ease-in-out 0.2s forwards',
'@keyframes fadeIn': {
'0%': { opacity: 0 },
'100%': { opacity: 1 },
},
}}
>
<Typography
align="center"
gutterBottom
>
Generating invite link
</Typography>
<CircularProgress />
</Box>
)}

{!inviteLinkLoading && inviteLink && (
<FormControl sx={{ mt: 2, mb: 3 }} fullWidth variant="outlined">
<InputLabel htmlFor="invite-link">Invite link</InputLabel>
<OutlinedInput
id="invite-link"
type="text"
defaultValue={inviteLink}
onFocus={(event) => event.target.select()}
readOnly
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="copy invite link"
onClick={copyInviteLink}
edge="end"
>
<ContentCopy />
</IconButton>
</InputAdornment>
}
label="Invite link"
/>
</FormControl>
)}
<Divider />
</Grid>
<Grid item xs={12}>
Expand All @@ -197,8 +270,8 @@ export function ManageUserDrawer({
disabled={
role.isProtected
? !globalPermissions(
Permission.EditPersonUserProtectedRoles
)
Permission.EditPersonUserProtectedRoles
)
: !globalPermissions(Permission.EditPersonUserStandardRoles)
}
onClick={toggleRoleSelection(role.roleName!)}
Expand Down
43 changes: 43 additions & 0 deletions src/caretogether-pwa/src/Hooks/useGlobalSnackBar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { atom, useRecoilState } from 'recoil';

export const showGlobalSnackBar = atom<string | null>({
key: 'showGlobalSnackBar',
default: null,
});

export function useGlobalSnackBar() {
const [message, setMessage] = useRecoilState(showGlobalSnackBar);

return {
message,
setAndShowGlobalSnackBar: (newMessage: string) => {
setMessage((currentMessage) => {
if (currentMessage === null) {
return newMessage;
}

// If newMessage is equal to the currentMessage, add a " (x)" at the end
// to indicate how many times that notification was shown.
const match = currentMessage
?.replace(newMessage, '')
.match(/^\s\((\d)\)$/);

const currentMessageIncludesCounter = match !== null;

const isADuplicatedMessage =
currentMessageIncludesCounter || currentMessage === newMessage;

const currentMessageNumber = match ? parseInt(match[1]) : 0;

if (isADuplicatedMessage) {
return `${newMessage} (${currentMessageNumber + 1})`;
}

return newMessage;
});
},
resetSnackBar: () => {
setMessage(null);
},
};
}
20 changes: 19 additions & 1 deletion src/caretogether-pwa/src/Shell/ShellRootLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Box, Container, useMediaQuery, useTheme } from '@mui/material';
import {
Box,
Container,
Snackbar,
useMediaQuery,
useTheme,
} from '@mui/material';
import { ShellBottomNavigation } from './ShellBottomNavigation';
import { ShellAppBar } from './ShellAppBar';
import { ShellSideNavigation } from './ShellSideNavigation';
import { useLocalStorage } from '../Hooks/useLocalStorage';
import React from 'react';
import { ProgressBackdrop } from './ProgressBackdrop';
import { useGlobalSnackBar } from '../Hooks/useGlobalSnackBar';

function ShellRootLayout({ children }: React.PropsWithChildren) {
const theme = useTheme();
Expand All @@ -17,6 +24,8 @@ function ShellRootLayout({ children }: React.PropsWithChildren) {

const drawerWidth = menuDrawerOpen ? 190 : 48;

const { message, resetSnackBar } = useGlobalSnackBar();

return (
<Box sx={{ flexGrow: 1 }}>
<ShellAppBar
Expand Down Expand Up @@ -51,6 +60,15 @@ function ShellRootLayout({ children }: React.PropsWithChildren) {
</Container>
</Box>
{!isDesktop && <ShellBottomNavigation />}

<Snackbar
key={message}
open={Boolean(message)}
autoHideDuration={5000}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
message={message}
onClose={resetSnackBar}
/>
</Box>
);
}
Expand Down
17 changes: 7 additions & 10 deletions src/caretogether-pwa/src/Volunteers/VolunteerApproval.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
InputBase,
SelectChangeEvent,
IconButton,
Snackbar,
Stack,
ToggleButton,
ToggleButtonGroup,
Expand Down Expand Up @@ -60,6 +59,7 @@ import { ProgressBackdrop } from '../Shell/ProgressBackdrop';
import { selectedLocationContextState } from '../Model/Data';
import { useAppNavigate } from '../Hooks/useAppNavigate';
import { VolunteerRoleApprovalStatusChip } from './VolunteerRoleApprovalStatusChip';
import { useGlobalSnackBar } from '../Hooks/useGlobalSnackBar';

//#region Role/Status Selection code
enum filterType {
Expand Down Expand Up @@ -605,14 +605,17 @@ function VolunteerApproval(props: { onOpen: () => void }) {
.filter((email) => typeof email !== 'undefined') as EmailAddress[];
}

const { setAndShowGlobalSnackBar } = useGlobalSnackBar();

function copyEmailAddresses() {
const emailAddresses = getSelectedFamiliesContactEmails();
navigator.clipboard.writeText(
emailAddresses.map((email) => email.address).join('; ')
);
setNoticeOpen(true);
setAndShowGlobalSnackBar(
`Found and copied ${getSelectedFamiliesContactEmails().length} email addresses for ${selectedFamilies.length} selected families to clipboard`
);
}
const [noticeOpen, setNoticeOpen] = useState(false);

const windowSize = useWindowSize();

Expand Down Expand Up @@ -680,13 +683,7 @@ function VolunteerApproval(props: { onOpen: () => void }) {
<SmsIcon sx={{ position: 'relative', top: 1 }} />
</IconButton>
)}
<Snackbar
open={noticeOpen}
autoHideDuration={5000}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
onClose={() => setNoticeOpen(false)}
message={`Found and copied ${getSelectedFamiliesContactEmails().length} email addresses for ${selectedFamilies.length} selected families to clipboard`}
/>

<Box
sx={{
display: 'flex',
Expand Down

0 comments on commit aeedd68

Please sign in to comment.