Your commit message
This commit is contained in:
13
src/App.jsx
13
src/App.jsx
@@ -6,6 +6,7 @@ import { useEffect, useState } from 'react'
|
||||
import { Outlet, useNavigate } from 'react-router-dom'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
import { registerCapacitorListeners } from './CapacitorListener'
|
||||
import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext'
|
||||
import { UserContext } from './contexts/UserContext'
|
||||
import { useResource } from './queries/ResourceQueries'
|
||||
import { AuthenticationProvider } from './service/AuthenticationService'
|
||||
@@ -93,14 +94,18 @@ function App() {
|
||||
return (
|
||||
<div className='min-h-screen'>
|
||||
<NetworkBanner />
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthenticationProvider />
|
||||
<ErrorProvider>
|
||||
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
||||
<NavBar />
|
||||
<Outlet />
|
||||
</UserContext.Provider>
|
||||
<ImpersonateUserProvider>
|
||||
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
||||
<NavBar />
|
||||
<Outlet />
|
||||
</UserContext.Provider>
|
||||
</ImpersonateUserProvider>
|
||||
</ErrorProvider>
|
||||
|
||||
{needRefresh && (
|
||||
<Snackbar open={showUpdateSnackbar}>
|
||||
<Typography level='body-md'>
|
||||
|
||||
16
src/contexts/ImpersonateUserContext.jsx
Normal file
16
src/contexts/ImpersonateUserContext.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
|
||||
const ImpersonateUserContext = createContext()
|
||||
|
||||
export const useImpersonateUser = () => useContext(ImpersonateUserContext)
|
||||
|
||||
export const ImpersonateUserProvider = ({ children }) => {
|
||||
const [impersonatedUser, setImpersonatedUser] = useState(null)
|
||||
return (
|
||||
<ImpersonateUserContext.Provider
|
||||
value={{ impersonatedUser, setImpersonatedUser }}
|
||||
>
|
||||
{children}
|
||||
</ImpersonateUserContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -97,12 +97,9 @@ const GetChoreDetailById = id => {
|
||||
headers: HEADERS(),
|
||||
})
|
||||
}
|
||||
const MarkChoreComplete = (id, note, completedDate, performer) => {
|
||||
const MarkChoreComplete = (id, body, completedDate, performer) => {
|
||||
var markChoreURL = `/chores/${id}/do`
|
||||
|
||||
const body = {
|
||||
note,
|
||||
}
|
||||
let completedDateFormated = ''
|
||||
if (completedDate) {
|
||||
completedDateFormated = `?completedDate=${new Date(
|
||||
@@ -220,6 +217,14 @@ const GetAllCircleMembers = async () => {
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
const UpdateMemberRole = async (memberId, role) => {
|
||||
return Fetch(`/circles/members/role`, {
|
||||
method: 'PUT',
|
||||
headers: HEADERS(),
|
||||
body: JSON.stringify({ role, memberId }),
|
||||
})
|
||||
}
|
||||
|
||||
const GetUserProfile = () => {
|
||||
return Fetch(`/users/profile`, {
|
||||
method: 'GET',
|
||||
@@ -540,6 +545,7 @@ export {
|
||||
UpdateChoreStatus,
|
||||
UpdateDueDate,
|
||||
UpdateLabel,
|
||||
UpdateMemberRole,
|
||||
UpdateNotificationTarget,
|
||||
UpdatePassword,
|
||||
UpdateThingState,
|
||||
|
||||
@@ -43,6 +43,28 @@ class ApiManager {
|
||||
|
||||
export const apiManager = new ApiManager()
|
||||
|
||||
export const getAssetURL = path => {
|
||||
const baseURL = apiManager.getApiURL()
|
||||
return `${baseURL}/assets/${path}`
|
||||
}
|
||||
export async function UploadFile(url, options) {
|
||||
if (!isTokenValid()) {
|
||||
Cookies.set('ca_redirect', window.location.pathname)
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
if (!options) {
|
||||
options = {}
|
||||
}
|
||||
const headers = HEADERS()
|
||||
options.headers = { Authorization: headers['Authorization'] }
|
||||
|
||||
const baseURL = apiManager.getApiURL()
|
||||
const fullURL = `${baseURL}${url}`
|
||||
|
||||
return fetch(fullURL, options)
|
||||
}
|
||||
|
||||
export async function Fetch(url, options) {
|
||||
if (!isTokenValid()) {
|
||||
Cookies.set('ca_redirect', window.location.pathname)
|
||||
|
||||
@@ -37,6 +37,8 @@ import { Divider } from '@mui/material'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useChoreDetails } from '../../queries/ChoreQueries.jsx'
|
||||
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
@@ -85,6 +87,7 @@ const ChoreView = () => {
|
||||
isLoading: isCircleMembersLoading,
|
||||
handleRefetch: handleCircleMembersRefetch,
|
||||
} = useCircleMembers()
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
const {
|
||||
data: choreData,
|
||||
@@ -181,7 +184,14 @@ const ChoreView = () => {
|
||||
}, 1000)
|
||||
|
||||
const id = setTimeout(() => {
|
||||
MarkChoreComplete(choreId, note, completedDate, null)
|
||||
MarkChoreComplete(
|
||||
choreId,
|
||||
impersonatedUser
|
||||
? { completedBy: impersonatedUser.userId, note }
|
||||
: { note },
|
||||
completedDate,
|
||||
null,
|
||||
)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json().then(data => {
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
import moment from 'moment'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useError } from '../../service/ErrorProvider'
|
||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
@@ -93,6 +94,8 @@ const ChoreCard = ({
|
||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
||||
const [timeoutId, setTimeoutId] = React.useState(null)
|
||||
const { userProfile } = React.useContext(UserContext)
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
const { showError } = useError()
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleMenuOutsideClick)
|
||||
@@ -187,7 +190,12 @@ const ChoreCard = ({
|
||||
}, 1000)
|
||||
|
||||
const id = setTimeout(() => {
|
||||
MarkChoreComplete(chore.id, null, null, null)
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json().then(data => {
|
||||
@@ -249,7 +257,7 @@ const ChoreCard = ({
|
||||
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
null,
|
||||
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
|
||||
new Date(newDate).toISOString(),
|
||||
null,
|
||||
).then(response => {
|
||||
@@ -272,7 +280,14 @@ const ChoreCard = ({
|
||||
})
|
||||
}
|
||||
const handleCompleteWithNote = note => {
|
||||
MarkChoreComplete(chore.id, note, null, null).then(response => {
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser
|
||||
? { note, completedBy: impersonatedUser.userId }
|
||||
: { note },
|
||||
null,
|
||||
null,
|
||||
).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMediaQuery } from '@mui/material'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ChoresGrouper } from '../../utils/Chores'
|
||||
import CalendarView from '../components/CalendarView'
|
||||
import WelcomeCard from './WelcomeCard'
|
||||
|
||||
const Sidepanel = ({ chores }) => {
|
||||
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
|
||||
@@ -30,61 +31,28 @@ const Sidepanel = ({ chores }) => {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Sheet
|
||||
variant='plain'
|
||||
sx={{
|
||||
p: 2,
|
||||
// borderRadius: 'sm',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
mr: 10,
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: 'sm',
|
||||
borderRadius: 20,
|
||||
|
||||
// minimum height to fit the content:
|
||||
height: '80vh',
|
||||
width: '290px',
|
||||
}}
|
||||
>
|
||||
{/* <Box
|
||||
<Box>
|
||||
<WelcomeCard chores={chores} />
|
||||
<Sheet
|
||||
variant='plain'
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
mr: 10,
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: 'sm',
|
||||
borderRadius: 20,
|
||||
height: '80vh',
|
||||
width: '290px',
|
||||
}}
|
||||
>
|
||||
<PieChart width={200} height={200}>
|
||||
<Pie
|
||||
data={dueDatePieChartData}
|
||||
dataKey='value'
|
||||
nameKey='label'
|
||||
innerRadius={30}
|
||||
paddingAngle={5}
|
||||
cornerRadius={5}
|
||||
>
|
||||
{dueDatePieChartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
|
||||
<Legend
|
||||
layout='horizontal'
|
||||
align='center'
|
||||
iconType='circle'
|
||||
iconSize={8}
|
||||
fontSize={12}
|
||||
formatter={(label, value) => `${label}: ${value.payload.value}`}
|
||||
wrapperStyle={{ paddingTop: 0, marginTop: 0 }} // Adjust padding and margin
|
||||
/>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</Box> */}
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<CalendarView chores={chores} />
|
||||
</Box>
|
||||
</Sheet>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<CalendarView chores={chores} />
|
||||
</Box>
|
||||
</Sheet>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
169
src/views/Chores/WelcomeCard.jsx
Normal file
169
src/views/Chores/WelcomeCard.jsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Avatar, Box, Button, Sheet, Typography } from '@mui/joy'
|
||||
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useCircleMembers } from '../../queries/UserQueries'
|
||||
import UserModal from '../Modals/Inputs/UserModal'
|
||||
const WelcomeCard = chores => {
|
||||
const { impersonatedUser, setImpersonatedUser } = useImpersonateUser()
|
||||
const [isAdmin, setIsAdmin] = useState(false)
|
||||
const { userProfile } = useContext(UserContext)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [summarytext, setSummaryText] = useState('')
|
||||
|
||||
const {
|
||||
data: circleMembersData,
|
||||
isLoading: isCircleMembersLoading,
|
||||
handleRefetch: handleCircleMembersRefetch,
|
||||
} = useCircleMembers()
|
||||
|
||||
useEffect(() => {
|
||||
if (userProfile && userProfile?.id) {
|
||||
const members = circleMembersData?.res || []
|
||||
const isUserAdmin = members.some(
|
||||
member =>
|
||||
member.userId === userProfile?.id &&
|
||||
(member.role === 'admin' || member.role === 'manager'),
|
||||
)
|
||||
members.forEach(m =>
|
||||
console.log('Member:', userProfile?.id, m.id, m.role),
|
||||
)
|
||||
console.log('Circle Members:', members)
|
||||
console.log(isUserAdmin)
|
||||
|
||||
setIsAdmin(isUserAdmin)
|
||||
}
|
||||
}, [userProfile, circleMembersData])
|
||||
if (!isAdmin) {
|
||||
return null
|
||||
} else if (isCircleMembersLoading || impersonatedUser === null) {
|
||||
return (
|
||||
<Sheet
|
||||
variant='plain'
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
mr: 10,
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: 'sm',
|
||||
borderRadius: 20,
|
||||
width: '290px',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ textAlign: 'center', width: '100%' }}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography level='title-md' sx={{ mb: 0.5 }}>
|
||||
Who's checking in?
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant='plain'
|
||||
color='primary'
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
size='sm'
|
||||
>
|
||||
Select User
|
||||
</Button>
|
||||
<UserModal
|
||||
isOpen={isModalOpen}
|
||||
performers={circleMembersData?.res}
|
||||
onSelect={user => {
|
||||
setImpersonatedUser(user)
|
||||
setIsModalOpen(false)
|
||||
}}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
</Box>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
variant='plain'
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
mr: 10,
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: 'sm',
|
||||
borderRadius: 20,
|
||||
width: '290px',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Box sx={{ mr: 2 }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
}}
|
||||
src={impersonatedUser?.image || impersonatedUser?.avatar}
|
||||
alt={impersonatedUser?.displayName || impersonatedUser?.name}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography level='title-md' sx={{ mb: 1, ml: 0.5 }}>
|
||||
{impersonatedUser?.displayName || impersonatedUser?.name}
|
||||
</Typography>
|
||||
{/* <Box sx={{ fontSize: 14, color: 'text.secondary', mb: 0.5 }}>
|
||||
5 chores assigned, 2 due soon
|
||||
</Box> */}
|
||||
<Box>
|
||||
<Button
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
setIsModalOpen(true)
|
||||
}}
|
||||
>
|
||||
Change User
|
||||
</Button>
|
||||
<Button
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
sx={{ ml: 0.5 }}
|
||||
onClick={() => {
|
||||
setImpersonatedUser(null)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<UserModal
|
||||
isOpen={isModalOpen}
|
||||
performers={circleMembersData?.res}
|
||||
onSelect={user => {
|
||||
setImpersonatedUser(user)
|
||||
setIsModalOpen(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false)
|
||||
}}
|
||||
/>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
export default WelcomeCard
|
||||
58
src/views/Modals/Inputs/UserModal.jsx
Normal file
58
src/views/Modals/Inputs/UserModal.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
List,
|
||||
ListItem,
|
||||
Modal,
|
||||
ModalDialog,
|
||||
ModalOverflow,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
|
||||
const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => {
|
||||
return (
|
||||
<Modal open={isOpen} onClose={onClose}>
|
||||
<ModalOverflow>
|
||||
<ModalDialog size='md' sx={{ minWidth: 360 }}>
|
||||
<Typography level='h4' sx={{ mb: 2 }}>
|
||||
Select User
|
||||
</Typography>
|
||||
<List sx={{ mb: 2 }}>
|
||||
{performers.map(user => (
|
||||
<ListItem
|
||||
key={user.id}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
onSelect(user)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Avatar
|
||||
size='lg'
|
||||
src={user.image || user.avatar}
|
||||
alt={user.displayName || user.name}
|
||||
/>
|
||||
<Typography>{user.displayName || user.name}</Typography>
|
||||
</Box>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button variant='outlined' color='neutral' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</ModalDialog>
|
||||
</ModalOverflow>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserModal
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
ListItem,
|
||||
Option,
|
||||
Select,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
@@ -28,6 +31,7 @@ import {
|
||||
JoinCircle,
|
||||
LeaveCircle,
|
||||
PutWebhookURL,
|
||||
UpdateMemberRole,
|
||||
UpdatePassword,
|
||||
} from '../../utils/Fetcher'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
@@ -46,6 +50,7 @@ const Settings = () => {
|
||||
const [circleMembers, setCircleMembers] = useState([])
|
||||
const [webhookURL, setWebhookURL] = useState(null)
|
||||
const [webhookError, setWebhookError] = useState(null)
|
||||
const [isAdmin, setIsAdmin] = useState(false)
|
||||
|
||||
const [changePasswordModal, setChangePasswordModal] = useState(false)
|
||||
useEffect(() => {
|
||||
@@ -70,6 +75,16 @@ const Settings = () => {
|
||||
})
|
||||
}, [])
|
||||
|
||||
// useEffect when circleMembers and userprofile:
|
||||
useEffect(() => {
|
||||
if (userProfile && userProfile.id) {
|
||||
const isUserAdmin = circleMembers.some(
|
||||
member => member.userId === userProfile.id && member.role === 'admin',
|
||||
)
|
||||
setIsAdmin(isUserAdmin)
|
||||
}
|
||||
}, [circleMembers, userProfile])
|
||||
|
||||
useEffect(() => {
|
||||
const hash = window.location.hash
|
||||
if (hash) {
|
||||
@@ -222,33 +237,110 @@ const Settings = () => {
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
{member.userId !== userProfile.id && member.isActive && (
|
||||
<Button
|
||||
disabled={
|
||||
circleMembers.find(m => userProfile.id == m.userId).role !==
|
||||
'admin'
|
||||
}
|
||||
variant='outlined'
|
||||
color='danger'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
const confirmed = confirm(
|
||||
`Are you sure you want to remove ${member.displayName} from your circle?`,
|
||||
)
|
||||
if (confirmed) {
|
||||
DeleteCircleMember(member.circleId, member.userId).then(
|
||||
resp => {
|
||||
if (resp.ok) {
|
||||
alert('Removed member successfully.')
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{member.userId !== userProfile.id && isAdmin && (
|
||||
<Select
|
||||
size='sm'
|
||||
sx={{ mr: 1 }}
|
||||
value={member.role}
|
||||
renderValue={() => (
|
||||
<Typography>
|
||||
{member.role.charAt(0).toUpperCase() +
|
||||
member.role.slice(1)}
|
||||
</Typography>
|
||||
)}
|
||||
onChange={(e, value) => {
|
||||
UpdateMemberRole(member.userId, value).then(resp => {
|
||||
if (resp.ok) {
|
||||
const newCircleMembers = circleMembers.map(m => {
|
||||
if (m.userId === member.userId) {
|
||||
m.role = value
|
||||
}
|
||||
return m
|
||||
})
|
||||
setCircleMembers(newCircleMembers)
|
||||
} else {
|
||||
alert('Failed to update role')
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
value: 'member',
|
||||
description: 'Just a regular member of the circle',
|
||||
},
|
||||
{
|
||||
value: 'manager',
|
||||
description:
|
||||
'Can impersonate users and perform actions on their behalf',
|
||||
},
|
||||
{
|
||||
value: 'admin',
|
||||
description: 'Full access to the circle',
|
||||
},
|
||||
].map((option, index) => (
|
||||
<Option value={option.value} key={index}>
|
||||
<ListItem
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'start',
|
||||
alignItems: 'start',
|
||||
width: '100%',
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='title-sm'
|
||||
sx={{ mb: 0, mt: 0, lineHeight: 1.1 }}
|
||||
>
|
||||
{option.value.charAt(0).toUpperCase() +
|
||||
option.value.slice(1)}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ mt: 0, mb: 0, lineHeight: 1.1 }}
|
||||
>
|
||||
{option.description}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
{userProfile.role === 'admin' &&
|
||||
member.userId !== userProfile.id &&
|
||||
member.isActive && (
|
||||
<Button
|
||||
disabled={
|
||||
circleMembers.find(m => userProfile.id == m.userId)
|
||||
.role !== 'admin'
|
||||
}
|
||||
variant='outlined'
|
||||
color='danger'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
const confirmed = confirm(
|
||||
`Are you sure you want to remove ${member.displayName} from your circle?`,
|
||||
)
|
||||
if (confirmed) {
|
||||
DeleteCircleMember(
|
||||
member.circleId,
|
||||
member.userId,
|
||||
).then(resp => {
|
||||
if (resp.ok) {
|
||||
alert('Removed member successfully.')
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
@@ -31,12 +31,12 @@ const RichTextEditor = ({
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
// Define compression options based on entity type
|
||||
// Define compression options based on entity type ( this need a revist later)
|
||||
const compressionOptions = {
|
||||
maxSizeMB: entityType === 'profile' ? 0.5 : 1, // Smaller size for profile images
|
||||
maxWidthOrHeight: entityType === 'profile' ? 320 : 1200, // Smaller dimensions for profile images
|
||||
useWebWorker: true,
|
||||
fileType: 'image/jpeg', // Always convert to JPEG
|
||||
fileType: 'image/jpeg',
|
||||
}
|
||||
|
||||
// Compress the image
|
||||
@@ -59,6 +59,7 @@ const RichTextEditor = ({
|
||||
formData.append('file', compressedJpegFile)
|
||||
formData.append('entityId', entityId)
|
||||
formData.append('entityType', entityType)
|
||||
|
||||
const response = await UploadFile('/assets/chore', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
@@ -83,7 +84,6 @@ const RichTextEditor = ({
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const url = resolvePhotoURL(data.url || data.sign)
|
||||
// Insert image into Quill
|
||||
|
||||
Reference in New Issue
Block a user