Merge pull request #3 from majicmaj/majicmaj-feature/refactor-labels-view-table
Feature - Refactor Labels Table to display the appearance of the label
This commit is contained in:
commit
78c3a6a186
2 changed files with 98 additions and 125 deletions
|
@ -2,13 +2,14 @@ import DeleteIcon from '@mui/icons-material/Delete'
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Container,
|
Container,
|
||||||
IconButton,
|
IconButton,
|
||||||
Table,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import React, { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import LabelModal from '../Modals/Inputs/LabelModal'
|
import LabelModal from '../Modals/Inputs/LabelModal'
|
||||||
import { useLabels } from './LabelQueries'
|
import { useLabels } from './LabelQueries'
|
||||||
|
|
||||||
|
@ -17,12 +18,15 @@ import { Add } from '@mui/icons-material'
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { DeleteLabel } from '../../utils/Fetcher'
|
import { DeleteLabel } from '../../utils/Fetcher'
|
||||||
|
import { getTextColorFromBackgroundColor } from '../../utils/LabelColors'
|
||||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||||
|
|
||||||
const LabelView = () => {
|
const LabelView = () => {
|
||||||
const { data: labels, isLabelsLoading, isError } = useLabels()
|
const { data: labels, isLabelsLoading, isError } = useLabels()
|
||||||
|
|
||||||
const [userLabels, setUserLabels] = useState([labels])
|
const [userLabels, setUserLabels] = useState([labels])
|
||||||
const [modalOpen, setModalOpen] = useState(false)
|
const [modalOpen, setModalOpen] = useState(false)
|
||||||
|
|
||||||
const [currentLabel, setCurrentLabel] = useState(null)
|
const [currentLabel, setCurrentLabel] = useState(null)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [confirmationModel, setConfirmationModel] = useState({})
|
const [confirmationModel, setConfirmationModel] = useState({})
|
||||||
|
@ -74,6 +78,7 @@ const LabelView = () => {
|
||||||
)
|
)
|
||||||
setUserLabels(updatedLabels)
|
setUserLabels(updatedLabels)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (labels) {
|
if (labels) {
|
||||||
setUserLabels(labels)
|
setUserLabels(labels)
|
||||||
|
@ -103,62 +108,49 @@ const LabelView = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth='md'>
|
<Container maxWidth='md'>
|
||||||
<Table aria-label='Manage Labels' stickyHeader hoverRow>
|
<div className='flex flex-col gap-2'>
|
||||||
<thead>
|
{userLabels.map(label => (
|
||||||
<tr>
|
<div
|
||||||
<th style={{ textAlign: 'center' }}>Label</th>
|
key={label}
|
||||||
<th style={{ textAlign: 'center' }}>Color</th>
|
className='grid w-full grid-cols-[1fr,auto,auto] rounded-lg border border-zinc-200/80 p-4 shadow-sm dark:bg-zinc-900'
|
||||||
<th style={{ textAlign: 'center' }}>Actions</th>
|
>
|
||||||
</tr>
|
<Chip
|
||||||
</thead>
|
variant='outlined'
|
||||||
<tbody>
|
color='primary'
|
||||||
{userLabels.map(label => (
|
size='lg'
|
||||||
<tr key={label.id}>
|
sx={{
|
||||||
<td
|
background: label.color,
|
||||||
onClick={() => {
|
borderColor: label.color,
|
||||||
Navigate('/my/chores', { state: { label: label.id } })
|
color: getTextColorFromBackgroundColor(label.color),
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
{label.name}
|
||||||
|
</Chip>
|
||||||
|
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='soft'
|
||||||
|
color='neutral'
|
||||||
|
onClick={() => handleEditLabel(label)}
|
||||||
|
startDecorator={<EditIcon />}
|
||||||
|
|
||||||
>
|
>
|
||||||
{label.name}
|
Edit
|
||||||
</td>
|
</Button>
|
||||||
<td
|
<IconButton
|
||||||
style={{
|
size='sm'
|
||||||
// center without display flex:
|
variant='soft'
|
||||||
textAlign: 'center',
|
onClick={() => handleDeleteLabel(label.id)}
|
||||||
alignItems: 'center',
|
color='danger'
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box
|
<DeleteIcon />
|
||||||
width={20}
|
</IconButton>
|
||||||
height={20}
|
</div>
|
||||||
borderRadius='50%'
|
</div>
|
||||||
sx={{
|
))}
|
||||||
backgroundColor: label.color,
|
</div>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton onClick={() => handleEditLabel(label)}>
|
|
||||||
<EditIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => handleDeleteClicked(label.id)}
|
|
||||||
color='danger'
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
{userLabels.length === 0 && (
|
{userLabels.length === 0 && (
|
||||||
<Typography textAlign='center' mt={2}>
|
<Typography textAlign='center' mt={2}>
|
||||||
|
@ -174,6 +166,7 @@ const LabelView = () => {
|
||||||
label={currentLabel}
|
label={currentLabel}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
@ -9,18 +10,16 @@ import {
|
||||||
Select,
|
Select,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
|
import { useQueryClient, useMutation } from 'react-query'
|
||||||
import React, { useEffect } from 'react'
|
|
||||||
import { useQueryClient } from 'react-query'
|
|
||||||
import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher'
|
import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher'
|
||||||
import LABEL_COLORS from '../../../utils/LabelColors'
|
import LABEL_COLORS from '../../../utils/LabelColors'
|
||||||
import { useLabels } from '../../Labels/LabelQueries'
|
import { useLabels } from '../../Labels/LabelQueries'
|
||||||
|
|
||||||
function LabelModal({ isOpen, onClose, onSave, label }) {
|
function LabelModal({ isOpen, onClose, label }) {
|
||||||
const [labelName, setLabelName] = React.useState('')
|
const [labelName, setLabelName] = useState('')
|
||||||
const [color, setColor] = React.useState('')
|
const [color, setColor] = useState('')
|
||||||
const [error, setError] = React.useState('')
|
const [error, setError] = useState('')
|
||||||
const { data: userLabels, isLoadingLabels } = useLabels()
|
const { data: userLabels = [] } = useLabels()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
// Populate the form fields when editing
|
// Populate the form fields when editing
|
||||||
|
@ -35,48 +34,48 @@ function LabelModal({ isOpen, onClose, onSave, label }) {
|
||||||
setError('')
|
setError('')
|
||||||
}, [label])
|
}, [label])
|
||||||
|
|
||||||
|
// Validation logic
|
||||||
const validateLabel = () => {
|
const validateLabel = () => {
|
||||||
if (!labelName || labelName.trim() === '') {
|
if (!labelName.trim()) {
|
||||||
setError('Name cannot be empty')
|
setError('Name cannot be empty')
|
||||||
return false
|
return false
|
||||||
} else if (
|
}
|
||||||
|
if (
|
||||||
userLabels.some(
|
userLabels.some(
|
||||||
userLabel => userLabel.name === labelName && userLabel.id !== label.id,
|
userLabel => userLabel.name === labelName && userLabel.id !== label?.id,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setError('Label with this name already exists')
|
setError('Label with this name already exists')
|
||||||
return false
|
return false
|
||||||
} else if (color === '') {
|
}
|
||||||
|
if (!color) {
|
||||||
setError('Please select a color')
|
setError('Please select a color')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
// Mutation for saving labels
|
||||||
if (!validateLabel()) {
|
const saveLabelMutation = useMutation(
|
||||||
return
|
newLabel =>
|
||||||
}
|
label
|
||||||
|
? UpdateLabel({ id: label.id, ...newLabel })
|
||||||
const saveAction = label
|
: CreateLabel(newLabel),
|
||||||
? UpdateLabel({ id: label.id, name: labelName, color })
|
{
|
||||||
: CreateLabel({ name: labelName, color })
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries('labels')
|
||||||
saveAction.then(res => {
|
|
||||||
if (res.error) {
|
|
||||||
console.log(res.error)
|
|
||||||
setError('Failed to save label. Please try again.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res.json().then(data => {
|
|
||||||
if (data.error) {
|
|
||||||
setError('Failed to save label. Please try again.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onSave({ id: data?.res?.id, name: labelName, color })
|
|
||||||
onClose()
|
onClose()
|
||||||
})
|
},
|
||||||
})
|
onError: () => {
|
||||||
|
setError('Failed to save label. Please try again.')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!validateLabel()) return
|
||||||
|
|
||||||
|
saveLabelMutation.mutate({ name: labelName, color })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -85,51 +84,39 @@ function LabelModal({ isOpen, onClose, onSave, label }) {
|
||||||
<Typography level='title-md' mb={1}>
|
<Typography level='title-md' mb={1}>
|
||||||
{label ? 'Edit Label' : 'Add Label'}
|
{label ? 'Edit Label' : 'Add Label'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Typography level='body-sm' alignSelf={'start'}>
|
<Typography gutterBottom level='body-sm' alignSelf='start'>
|
||||||
Name
|
Name
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
margin='normal'
|
|
||||||
required
|
|
||||||
fullWidth
|
fullWidth
|
||||||
name='labelName'
|
|
||||||
type='text'
|
|
||||||
id='labelName'
|
id='labelName'
|
||||||
value={labelName}
|
value={labelName}
|
||||||
onChange={e => setLabelName(e.target.value)}
|
onChange={e => setLabelName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{/* Color Selection */}
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Typography level='body-sm' alignSelf={'start'}>
|
<Typography gutterBottom level='body-sm' alignSelf='start'>
|
||||||
Color:
|
Color
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label='Color'
|
|
||||||
value={color}
|
value={color}
|
||||||
|
onChange={(e, value) => value && setColor(value)}
|
||||||
renderValue={selected => (
|
renderValue={selected => (
|
||||||
<Typography
|
<Typography
|
||||||
key={selected.value}
|
|
||||||
startDecorator={
|
startDecorator={
|
||||||
<Box
|
<Box
|
||||||
className='h-4 w-4'
|
className='h-4 w-4'
|
||||||
borderRadius={10}
|
borderRadius={10}
|
||||||
sx={{
|
sx={{ background: selected.value }}
|
||||||
background: selected.value,
|
|
||||||
shadow: { xs: 1 },
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{selected.label}
|
{selected.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
onChange={(e, value) => {
|
|
||||||
value && setColor(value)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{LABEL_COLORS.map(val => (
|
{LABEL_COLORS.map(val => (
|
||||||
<Option key={val.value} value={val.value}>
|
<Option key={val.value} value={val.value}>
|
||||||
|
@ -138,31 +125,24 @@ function LabelModal({ isOpen, onClose, onSave, label }) {
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
borderRadius={10}
|
borderRadius={10}
|
||||||
sx={{
|
sx={{ background: val.value }}
|
||||||
background: val.value,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Typography
|
<Typography sx={{ ml: 1 }} variant='caption'>
|
||||||
sx={{
|
|
||||||
ml: 1,
|
|
||||||
color: 'text.secondary',
|
|
||||||
}}
|
|
||||||
variant='caption'
|
|
||||||
>
|
|
||||||
{val.name}
|
{val.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
{error && (
|
|
||||||
<Typography color='warning' level='body-sm'>
|
|
||||||
{error}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
{error && (
|
||||||
|
<Typography color='warning' level='body-sm'>
|
||||||
|
{error}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box display='flex' justifyContent='space-around' mt={1}>
|
||||||
<Button onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
<Button onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||||
{label ? 'Save Changes' : 'Add Label'}
|
{label ? 'Save Changes' : 'Add Label'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
Loading…
Add table
Reference in a new issue