Add Support for LabelV2, Add LabelModal and LabelView.
Add React Query
This commit is contained in:
parent
5e590bfe9f
commit
42182371ff
18 changed files with 839 additions and 71 deletions
|
@ -1,3 +1,4 @@
|
|||
import { Add } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
@ -11,6 +12,7 @@ import {
|
|||
Input,
|
||||
List,
|
||||
ListItem,
|
||||
MenuItem,
|
||||
Option,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
|
@ -34,8 +36,10 @@ import {
|
|||
SaveChore,
|
||||
} from '../../utils/Fetcher'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
import FreeSoloCreateOption from '../components/AutocompleteSelect'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/LabelColors'
|
||||
import { useLabels } from '../Labels/LabelQueries'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import LabelModal from '../Modals/Inputs/LabelModal'
|
||||
import RepeatSection from './RepeatSection'
|
||||
const ASSIGN_STRATEGIES = [
|
||||
'random',
|
||||
|
@ -66,6 +70,7 @@ const ChoreEdit = () => {
|
|||
const [frequency, setFrequency] = useState(1)
|
||||
const [frequencyMetadata, setFrequencyMetadata] = useState({})
|
||||
const [labels, setLabels] = useState([])
|
||||
const [labelsV2, setLabelsV2] = useState([])
|
||||
const [allUserThings, setAllUserThings] = useState([])
|
||||
const [thingTrigger, setThingTrigger] = useState(null)
|
||||
const [isThingValid, setIsThingValid] = useState(false)
|
||||
|
@ -82,6 +87,9 @@ const ChoreEdit = () => {
|
|||
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||
const [snackbarColor, setSnackbarColor] = useState('warning')
|
||||
const [addLabelModalOpen, setAddLabelModalOpen] = useState(false)
|
||||
const { data: userLabels, isLoading: isUserLabelsLoading } = useLabels()
|
||||
|
||||
const Navigate = useNavigate()
|
||||
|
||||
const HandleValidateChore = () => {
|
||||
|
@ -172,7 +180,8 @@ const ChoreEdit = () => {
|
|||
isRolling: isRolling,
|
||||
isActive: isActive,
|
||||
notification: isNotificable,
|
||||
labels: labels,
|
||||
labels: labels.map(l => l.name),
|
||||
labelsV2: labelsV2,
|
||||
notificationMetadata: notificationMetadata,
|
||||
thingTrigger: thingTrigger,
|
||||
}
|
||||
|
@ -226,8 +235,9 @@ const ChoreEdit = () => {
|
|||
setFrequency(data.res.frequency)
|
||||
|
||||
setNotificationMetadata(JSON.parse(data.res.notificationMetadata))
|
||||
setLabels(data.res.labels ? data.res.labels.split(',') : [])
|
||||
// setLabels(data.res.labels ? data.res.labels.split(',') : [])
|
||||
|
||||
setLabelsV2(data.res.labelsV2)
|
||||
setAssignStrategy(
|
||||
data.res.assignStrategy
|
||||
? data.res.assignStrategy
|
||||
|
@ -275,6 +285,14 @@ const ChoreEdit = () => {
|
|||
}
|
||||
}, [])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (userLabels && userLabels.length == 0 && labelsV2.length == 0) {
|
||||
// return
|
||||
// }
|
||||
// const labelIds = labelsV2.map(l => l.id)
|
||||
// setLabelsV2(userLabels.filter(l => labelIds.indexOf(l.id) > -1))
|
||||
// }, [userLabels, labelsV2])
|
||||
|
||||
useEffect(() => {
|
||||
// if frequancy type change to somthing need a due date then set it to the current date:
|
||||
if (!NO_DUE_DATE_REQUIRED_TYPE.includes(frequencyType) && !dueDate) {
|
||||
|
@ -329,6 +347,7 @@ const ChoreEdit = () => {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth='md'>
|
||||
{/* <Typography level='h3' mb={1.5}>
|
||||
|
@ -726,8 +745,9 @@ const ChoreEdit = () => {
|
|||
<Typography level='h5'>
|
||||
Things to remember about this chore or to tag it
|
||||
</Typography>
|
||||
<FreeSoloCreateOption
|
||||
options={labels}
|
||||
{/* <FreeSoloCreateOption
|
||||
options={[...labels, 'test']}
|
||||
selected={labels}
|
||||
onSelectChange={changes => {
|
||||
const newLabels = []
|
||||
changes.map(change => {
|
||||
|
@ -741,7 +761,99 @@ const ChoreEdit = () => {
|
|||
})
|
||||
setLabels(newLabels)
|
||||
}}
|
||||
/>
|
||||
/> */}
|
||||
<Select
|
||||
multiple
|
||||
onChange={(event, newValue) => {
|
||||
setLabelsV2(userLabels.filter(l => newValue.indexOf(l.name) > -1))
|
||||
}}
|
||||
value={labelsV2.map(l => l.name)}
|
||||
renderValue={selected => (
|
||||
<Box sx={{ display: 'flex', gap: '0.25rem' }}>
|
||||
{labelsV2.map(selectedOption => {
|
||||
return (
|
||||
<Chip
|
||||
variant='soft'
|
||||
color='primary'
|
||||
key={selectedOption.id}
|
||||
size='lg'
|
||||
sx={{
|
||||
background: selectedOption.color,
|
||||
color: getTextColorFromBackgroundColor(
|
||||
selectedOption.color,
|
||||
),
|
||||
}}
|
||||
>
|
||||
{selectedOption.name}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
sx={{ minWidth: '15rem' }}
|
||||
slotProps={{
|
||||
listbox: {
|
||||
sx: {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{userLabels &&
|
||||
userLabels
|
||||
// .map(l => l.name)
|
||||
.map(label => (
|
||||
<Option key={label.id + label.name} value={label.name}>
|
||||
<div
|
||||
style={{
|
||||
width: '20 px',
|
||||
height: '20 px',
|
||||
borderRadius: '50%',
|
||||
background: label.color,
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</Option>
|
||||
))}
|
||||
<MenuItem
|
||||
key={'addNewLabel'}
|
||||
value={' New Label'}
|
||||
onClick={() => {
|
||||
setAddLabelModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<Add />
|
||||
Add New Label
|
||||
</MenuItem>
|
||||
</Select>
|
||||
{/* <Card>
|
||||
<List
|
||||
orientation='horizontal'
|
||||
wrap
|
||||
sx={{
|
||||
'--List-gap': '8px',
|
||||
'--ListItem-radius': '20px',
|
||||
}}
|
||||
>
|
||||
{labels?.map((label, index) => (
|
||||
<ListItem key={label}>
|
||||
<Chip
|
||||
onClick={() => {
|
||||
setLabels(labels.filter(l => l !== label))
|
||||
}}
|
||||
checked={true}
|
||||
overlay
|
||||
variant='soft'
|
||||
color='primary'
|
||||
size='lg'
|
||||
endDecorator={<Cancel />}
|
||||
>
|
||||
{label}
|
||||
</Chip>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Card> */}
|
||||
</Box>
|
||||
{choreId > 0 && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
|
@ -822,6 +934,16 @@ const ChoreEdit = () => {
|
|||
</Button>
|
||||
</Sheet>
|
||||
<ConfirmationModal config={confirmModelConfig} />
|
||||
{addLabelModalOpen && (
|
||||
<LabelModal
|
||||
isOpen={addLabelModalOpen}
|
||||
onSave={label => {
|
||||
setLabels([...labels, label])
|
||||
setAddLabelModalOpen(false)
|
||||
}}
|
||||
onClose={() => setAddLabelModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{/* <ChoreHistory ChoreHistory={choresHistory} UsersData={performers} /> */}
|
||||
<Snackbar
|
||||
open={isSnackbarOpen}
|
||||
|
|
|
@ -43,6 +43,7 @@ import {
|
|||
SkipChore,
|
||||
UpdateChorePriority,
|
||||
} from '../../utils/Fetcher'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/LabelColors'
|
||||
import Priorities from '../../utils/Priorities'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
const IconCard = styled('div')({
|
||||
|
@ -264,6 +265,22 @@ const ChoreView = () => {
|
|||
? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}`
|
||||
: 'N/A'}
|
||||
</Chip>
|
||||
{/* show each label : */}
|
||||
{chore?.labelsV2?.map((label, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
ml: index === 0 ? 0 : 0.5,
|
||||
top: 2,
|
||||
zIndex: 1,
|
||||
backgroundColor: label?.color,
|
||||
color: getTextColorFromBackgroundColor(label?.color),
|
||||
}}
|
||||
>
|
||||
{label?.name}
|
||||
</Chip>
|
||||
))}
|
||||
</Box>
|
||||
<Box>
|
||||
<Sheet
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
SkipChore,
|
||||
UpdateChoreAssignee,
|
||||
} from '../../utils/Fetcher'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/LabelColors'
|
||||
import { Fetch } from '../../utils/TokenManager'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import DateModal from '../Modals/Inputs/DateModal'
|
||||
|
@ -58,6 +59,7 @@ const ChoreCard = ({
|
|||
performers,
|
||||
onChoreUpdate,
|
||||
onChoreRemove,
|
||||
userLabels,
|
||||
sx,
|
||||
viewOnly,
|
||||
}) => {
|
||||
|
@ -407,7 +409,7 @@ const ChoreCard = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box key={chore.id + '-box'}>
|
||||
<Chip
|
||||
variant='soft'
|
||||
sx={{
|
||||
|
@ -455,6 +457,7 @@ const ChoreCard = ({
|
|||
// backgroundColor: 'white',
|
||||
boxShadow: 'sm',
|
||||
borderRadius: 20,
|
||||
key: `${chore.id}-card`,
|
||||
|
||||
// mb: 2,
|
||||
}}
|
||||
|
@ -485,7 +488,7 @@ const ChoreCard = ({
|
|||
</Chip>
|
||||
</Typography>
|
||||
)}
|
||||
<Box>
|
||||
<Box key={`${chore.id}-labels`}>
|
||||
{chore.priority > 0 && (
|
||||
<Chip
|
||||
sx={{
|
||||
|
@ -505,22 +508,27 @@ const ChoreCard = ({
|
|||
P{chore.priority}
|
||||
</Chip>
|
||||
)}
|
||||
{chore.labels?.split(',').map((label, index) => (
|
||||
<Chip
|
||||
variant='solid'
|
||||
key={label}
|
||||
color='primary'
|
||||
sx={{
|
||||
position: 'relative',
|
||||
ml: index === 0 ? 0 : 0.5,
|
||||
top: 2,
|
||||
zIndex: 1,
|
||||
}}
|
||||
startDecorator={getIconForLabel(label)}
|
||||
>
|
||||
{label}
|
||||
</Chip>
|
||||
))}
|
||||
{chore.labelsV2?.map((l, index) => {
|
||||
return (
|
||||
<Chip
|
||||
variant='solid'
|
||||
key={l.id}
|
||||
color='primary'
|
||||
sx={{
|
||||
position: 'relative',
|
||||
ml: index === 0 ? 0 : 0.5,
|
||||
top: 2,
|
||||
zIndex: 1,
|
||||
backgroundColor: l?.color,
|
||||
color: getTextColorFromBackgroundColor(l?.color),
|
||||
}}
|
||||
|
||||
// startDecorator={getIconForLabel(label)}
|
||||
>
|
||||
{l?.name}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -757,7 +765,7 @@ const ChoreCard = ({
|
|||
</Typography>
|
||||
</Snackbar>
|
||||
</Card>
|
||||
</>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -21,8 +21,10 @@ import Fuse from 'fuse.js'
|
|||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { GetAllUsers, GetChores, GetUserProfile } from '../../utils/Fetcher'
|
||||
import { useChores } from '../../queries/ChoreQueries'
|
||||
import { GetAllUsers, GetUserProfile } from '../../utils/Fetcher'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
import { useLabels } from '../Labels/LabelQueries'
|
||||
import ChoreCard from './ChoreCard'
|
||||
|
||||
const MyChores = () => {
|
||||
|
@ -38,6 +40,8 @@ const MyChores = () => {
|
|||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const menuRef = useRef(null)
|
||||
const Navigate = useNavigate()
|
||||
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
||||
const { data: choresData, isLoading: choresLoading } = useChores()
|
||||
const choreSorter = (a, b) => {
|
||||
// 1. Handle null due dates (always last):
|
||||
if (!a.nextDueDate && !b.nextDueDate) return 0 // Both null, no order
|
||||
|
@ -74,14 +78,6 @@ const MyChores = () => {
|
|||
setUserProfile(data.res)
|
||||
})
|
||||
}
|
||||
GetChores()
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
data.res.sort(choreSorter)
|
||||
setChores(data.res)
|
||||
|
||||
setFilteredChores(data.res)
|
||||
})
|
||||
|
||||
GetAllUsers()
|
||||
.then(response => response.json())
|
||||
|
@ -94,6 +90,15 @@ const MyChores = () => {
|
|||
setActiveUserId(currentUser.id)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (choresData) {
|
||||
const sortedChores = choresData.res.sort(choreSorter)
|
||||
setChores(sortedChores)
|
||||
setFilteredChores(sortedChores)
|
||||
}
|
||||
}, [choresData, choresLoading])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleMenuOutsideClick)
|
||||
return () => {
|
||||
|
@ -160,12 +165,21 @@ const MyChores = () => {
|
|||
|
||||
const searchOptions = {
|
||||
// keys to search in
|
||||
keys: ['name', 'labels'],
|
||||
keys: ['name', 'raw_label'],
|
||||
includeScore: true, // Optional: if you want to see how well each result matched the search term
|
||||
isCaseSensitive: false,
|
||||
findAllMatches: true,
|
||||
}
|
||||
const fuse = new Fuse(chores, searchOptions)
|
||||
|
||||
const fuse = new Fuse(
|
||||
chores.map(c => ({
|
||||
...c,
|
||||
raw_label: c.labelsV2
|
||||
.map(l => userLabels.find(x => (x.id = l.id)).name)
|
||||
.join(' '),
|
||||
})),
|
||||
searchOptions,
|
||||
)
|
||||
|
||||
const handleSearchChange = e => {
|
||||
const search = e.target.value
|
||||
|
@ -180,7 +194,12 @@ const MyChores = () => {
|
|||
setFilteredChores(fuse.search(term).map(result => result.item))
|
||||
}
|
||||
|
||||
if (userProfile === null) {
|
||||
if (
|
||||
userProfile === null ||
|
||||
userLabelsLoading ||
|
||||
performers.length === 0 ||
|
||||
choresLoading
|
||||
) {
|
||||
return <LoadingComponent />
|
||||
}
|
||||
|
||||
|
@ -326,6 +345,7 @@ const MyChores = () => {
|
|||
onChoreUpdate={handleChoreUpdated}
|
||||
onChoreRemove={handleChoreDeleted}
|
||||
performers={performers}
|
||||
userLabels={userLabels}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
|
8
src/views/Labels/LabelQueries.jsx
Normal file
8
src/views/Labels/LabelQueries.jsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { useQuery } from 'react-query'
|
||||
import { GetLabels } from '../../utils/Fetcher'
|
||||
|
||||
export const useLabels = () => {
|
||||
return useQuery('labels', GetLabels, {
|
||||
initialData: [],
|
||||
})
|
||||
}
|
178
src/views/Labels/LabelView.jsx
Normal file
178
src/views/Labels/LabelView.jsx
Normal file
|
@ -0,0 +1,178 @@
|
|||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
Container,
|
||||
IconButton,
|
||||
Table,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import LabelModal from '../Modals/Inputs/LabelModal'
|
||||
import { useLabels } from './LabelQueries'
|
||||
|
||||
// import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Add } from '@mui/icons-material'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { DeleteLabel } from '../../utils/Fetcher'
|
||||
|
||||
const LabelView = () => {
|
||||
const { data: labels, isLabelsLoading, isError } = useLabels()
|
||||
const [userLabels, setUserLabels] = useState([labels])
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [currentLabel, setCurrentLabel] = useState(null) // Label being edited or null for new label
|
||||
const queryClient = useQueryClient()
|
||||
const handleAddLabel = () => {
|
||||
setCurrentLabel(null) // Adding a new label
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
const handleEditLabel = label => {
|
||||
setCurrentLabel(label) // Editing an existing label
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
const handleDeleteLabel = id => {
|
||||
DeleteLabel(id).then(res => {
|
||||
// Invalidate and refetch labels after deleting a label
|
||||
const updatedLabels = userLabels.filter(label => label.id !== id)
|
||||
setUserLabels(updatedLabels)
|
||||
|
||||
queryClient.invalidateQueries('labels')
|
||||
})
|
||||
// Implement deletion logic here
|
||||
}
|
||||
|
||||
const handleSaveLabel = newOrUpdatedLabel => {
|
||||
queryClient.invalidateQueries('labels')
|
||||
setModalOpen(false)
|
||||
const updatedLabels = userLabels.map(label =>
|
||||
label.id === newOrUpdatedLabel.id ? newOrUpdatedLabel : label,
|
||||
)
|
||||
setUserLabels(updatedLabels)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (labels) {
|
||||
setUserLabels(labels)
|
||||
}
|
||||
}, [labels])
|
||||
|
||||
if (isLabelsLoading) {
|
||||
return (
|
||||
<Box
|
||||
display='flex'
|
||||
justifyContent='center'
|
||||
alignItems='center'
|
||||
height='100vh'
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Typography color='danger' textAlign='center'>
|
||||
Failed to load labels. Please try again.
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth='md'>
|
||||
<Table aria-label='Manage Labels' stickyHeader hoverRow>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'center' }}>Label</th>
|
||||
<th style={{ textAlign: 'center' }}>Color</th>
|
||||
<th style={{ textAlign: 'center' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userLabels.map(label => (
|
||||
<tr key={label.id}>
|
||||
<td>{label.name}</td>
|
||||
<td
|
||||
style={{
|
||||
// center without display flex:
|
||||
textAlign: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={20}
|
||||
height={20}
|
||||
borderRadius='50%'
|
||||
sx={{
|
||||
backgroundColor: label.color,
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={() => handleEditLabel(label)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => handleDeleteLabel(label.id)}
|
||||
color='danger'
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
{userLabels.length === 0 && (
|
||||
<Typography textAlign='center' mt={2}>
|
||||
No labels available. Add a new label to get started.
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{modalOpen && (
|
||||
<LabelModal
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onSave={handleSaveLabel}
|
||||
label={currentLabel}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 10,
|
||||
p: 2, // padding
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 2,
|
||||
'z-index': 1000,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
color='primary'
|
||||
variant='solid'
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
width: 50,
|
||||
height: 50,
|
||||
}}
|
||||
onClick={handleAddLabel}
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default LabelView
|
0
src/views/Labels/Labels.jsx
Normal file
0
src/views/Labels/Labels.jsx
Normal file
175
src/views/Modals/Inputs/LabelModal.jsx
Normal file
175
src/views/Modals/Inputs/LabelModal.jsx
Normal file
|
@ -0,0 +1,175 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalDialog,
|
||||
Option,
|
||||
Select,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher'
|
||||
import LABEL_COLORS from '../../../utils/LabelColors'
|
||||
import { useLabels } from '../../Labels/LabelQueries'
|
||||
|
||||
function LabelModal({ isOpen, onClose, onSave, label }) {
|
||||
const [labelName, setLabelName] = React.useState('')
|
||||
const [color, setColor] = React.useState('')
|
||||
const [error, setError] = React.useState('')
|
||||
const { data: userLabels, isLoadingLabels } = useLabels()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Populate the form fields when editing
|
||||
useEffect(() => {
|
||||
if (label) {
|
||||
setLabelName(label.name)
|
||||
setColor(label.color)
|
||||
} else {
|
||||
setLabelName('')
|
||||
setColor('')
|
||||
}
|
||||
setError('')
|
||||
}, [label])
|
||||
|
||||
const validateLabel = () => {
|
||||
if (!labelName || labelName.trim() === '') {
|
||||
setError('Name cannot be empty')
|
||||
return false
|
||||
} else if (
|
||||
!label ||
|
||||
userLabels.some(
|
||||
userLabel => userLabel.name === labelName && userLabel.id !== label.id,
|
||||
)
|
||||
) {
|
||||
setError('Label with this name already exists')
|
||||
return false
|
||||
} else if (color === '') {
|
||||
setError('Please select a color')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (!validateLabel()) {
|
||||
return
|
||||
}
|
||||
|
||||
const saveAction = label
|
||||
? UpdateLabel({ id: label.id, name: labelName, color })
|
||||
: CreateLabel({ name: labelName, color })
|
||||
|
||||
saveAction.then(res => {
|
||||
if (res.error) {
|
||||
console.log(res.error)
|
||||
setError('Failed to save label. Please try again.')
|
||||
return
|
||||
}
|
||||
queryClient.invalidateQueries('labels').then(() => {
|
||||
onSave({ id: label?.id, name: labelName, color })
|
||||
onClose()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} onClose={onClose}>
|
||||
<ModalDialog>
|
||||
<Typography level='title-md' mb={1}>
|
||||
{label ? 'Edit Label' : 'Add Label'}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<Typography level='body-sm' alignSelf={'start'}>
|
||||
Name
|
||||
</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
name='labelName'
|
||||
type='text'
|
||||
id='labelName'
|
||||
value={labelName}
|
||||
onChange={e => setLabelName(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* Color Selection */}
|
||||
<FormControl>
|
||||
<Typography level='body-sm' alignSelf={'start'}>
|
||||
Color:
|
||||
</Typography>
|
||||
|
||||
<Select
|
||||
label='Color'
|
||||
value={color}
|
||||
renderValue={selected => (
|
||||
<Typography
|
||||
key={selected.value}
|
||||
startDecorator={
|
||||
<Box
|
||||
className='h-4 w-4'
|
||||
borderRadius={10}
|
||||
sx={{
|
||||
background: selected.value,
|
||||
shadow: { xs: 1 },
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{selected.label}
|
||||
</Typography>
|
||||
)}
|
||||
onChange={(e, value) => {
|
||||
value && setColor(value)
|
||||
}}
|
||||
>
|
||||
{LABEL_COLORS.map(val => (
|
||||
<Option key={val.value} value={val.value}>
|
||||
<Box className='flex items-center justify-between'>
|
||||
<Box
|
||||
width={20}
|
||||
height={20}
|
||||
borderRadius={10}
|
||||
sx={{
|
||||
background: val.value,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
ml: 1,
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
variant='caption'
|
||||
>
|
||||
{val.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
{error && (
|
||||
<Typography color='warning' level='body-sm'>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||
{label ? 'Save Changes' : 'Add Label'}
|
||||
</Button>
|
||||
<Button onClick={onClose} variant='outlined'>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</ModalDialog>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default LabelModal
|
|
@ -7,14 +7,18 @@ import * as React from 'react'
|
|||
|
||||
const filter = createFilterOptions()
|
||||
|
||||
export default function FreeSoloCreateOption({ options, onSelectChange }) {
|
||||
export default function FreeSoloCreateOption({
|
||||
options,
|
||||
onSelectChange,
|
||||
selected,
|
||||
}) {
|
||||
React.useEffect(() => {
|
||||
setValue(options)
|
||||
}, [options])
|
||||
|
||||
const [value, setValue] = React.useState([])
|
||||
const [value, setValue] = React.useState([selected])
|
||||
const [selectOptions, setSelectOptions] = React.useState(
|
||||
options ? options : [],
|
||||
selected ? selected : [],
|
||||
)
|
||||
return (
|
||||
<FormControl id='free-solo-with-text-demo'>
|
||||
|
@ -38,26 +42,27 @@ export default function FreeSoloCreateOption({ options, onSelectChange }) {
|
|||
}
|
||||
onSelectChange(newValue)
|
||||
}}
|
||||
filterOptions={(options, params) => {
|
||||
const filtered = filter(options, params)
|
||||
filterOptions={(selected, params) => {
|
||||
const filtered = filter(selected, params)
|
||||
|
||||
const { inputValue } = params
|
||||
// Suggest the creation of a new value
|
||||
const isExisting = options.some(option => inputValue === option.title)
|
||||
const isExisting = selected.some(
|
||||
option => inputValue === option.title,
|
||||
)
|
||||
if (inputValue !== '' && !isExisting) {
|
||||
filtered.push({
|
||||
inputValue,
|
||||
title: `Add "${inputValue}"`,
|
||||
})
|
||||
}
|
||||
|
||||
return filtered
|
||||
}}
|
||||
selectOnFocus
|
||||
clearOnBlur
|
||||
handleHomeEndKeys
|
||||
// freeSolo
|
||||
options={selectOptions}
|
||||
options={options}
|
||||
getOptionLabel={option => {
|
||||
// Value selected with enter, right from the input
|
||||
if (typeof option === 'string') {
|
||||
|
|
|
@ -2,7 +2,7 @@ import Logo from '@/assets/logo.svg'
|
|||
import {
|
||||
AccountBox,
|
||||
HomeOutlined,
|
||||
ListAltRounded,
|
||||
ListAlt,
|
||||
Logout,
|
||||
MenuRounded,
|
||||
Message,
|
||||
|
@ -30,6 +30,7 @@ const links = [
|
|||
label: 'Home',
|
||||
icon: <HomeOutlined />,
|
||||
},
|
||||
|
||||
// {
|
||||
// to: '/chores',
|
||||
// label: 'Desktop View',
|
||||
|
@ -40,6 +41,11 @@ const links = [
|
|||
label: 'Things',
|
||||
icon: <Widgets />,
|
||||
},
|
||||
{
|
||||
to: 'labels',
|
||||
label: 'Labels',
|
||||
icon: <ListAlt />,
|
||||
},
|
||||
{
|
||||
to: '/settings#sharing',
|
||||
label: 'Sharing',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue