Add Support for LabelV2, Add LabelModal and LabelView.
Add React Query
This commit is contained in:
commit
ff78cabf2b
20 changed files with 854 additions and 73 deletions
118
package-lock.json
generated
118
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "donetick",
|
||||
"version": "0.1.72",
|
||||
"version": "0.1.78",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "donetick",
|
||||
"version": "0.1.72",
|
||||
"version": "0.1.78",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
|
@ -23,7 +23,9 @@
|
|||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-query": "^3.39.3",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"reactjs-social-login": "^2.6.3",
|
||||
"vite-plugin-pwa": "^0.20.0"
|
||||
},
|
||||
|
@ -4142,6 +4144,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"node_modules/big-integer": {
|
||||
"version": "1.6.52",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
||||
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
|
||||
"license": "Unlicense",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
|
@ -4182,6 +4193,22 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/broadcast-channel": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
|
||||
"integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"detect-node": "^2.1.0",
|
||||
"js-sha3": "0.8.0",
|
||||
"microseconds": "0.2.0",
|
||||
"nano-time": "1.0.0",
|
||||
"oblivious-set": "1.0.0",
|
||||
"rimraf": "3.0.2",
|
||||
"unload": "2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.23.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz",
|
||||
|
@ -4664,6 +4691,12 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
|
@ -6360,6 +6393,12 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-sha3": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
|
||||
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
@ -6574,6 +6613,16 @@
|
|||
"sourcemap-codec": "^1.4.8"
|
||||
}
|
||||
},
|
||||
"node_modules/match-sorter": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz",
|
||||
"integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.8",
|
||||
"remove-accents": "0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
@ -6594,6 +6643,12 @@
|
|||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/microseconds": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
|
||||
"integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
|
@ -6665,6 +6720,15 @@
|
|||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nano-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
|
||||
"integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"big-integer": "^1.6.16"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
|
@ -6864,6 +6928,12 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/oblivious-set": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
|
||||
"integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
@ -7481,6 +7551,32 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/react-query": {
|
||||
"version": "3.39.3",
|
||||
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
|
||||
"integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"broadcast-channel": "^3.4.1",
|
||||
"match-sorter": "^6.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.21.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.1.tgz",
|
||||
|
@ -7515,6 +7611,7 @@
|
|||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
|
@ -7673,6 +7770,12 @@
|
|||
"jsesc": "bin/jsesc"
|
||||
}
|
||||
},
|
||||
"node_modules/remove-accents": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
|
||||
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
|
@ -7728,7 +7831,6 @@
|
|||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
|
@ -8857,6 +8959,16 @@
|
|||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unload": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
|
||||
"integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.6.2",
|
||||
"detect-node": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/upath": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "donetick",
|
||||
"private": true,
|
||||
"version": "0.1.78",
|
||||
"version": "0.1.79",
|
||||
"type": "module",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
|
@ -35,7 +35,9 @@
|
|||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-query": "^3.39.3",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"reactjs-social-login": "^2.6.3",
|
||||
"vite-plugin-pwa": "^0.20.0"
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@ import NavBar from '@/views/components/NavBar'
|
|||
import { Button, Snackbar, Typography, useColorScheme } from '@mui/joy'
|
||||
import Tracker from '@openreplay/tracker'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
import { UserContext } from './contexts/UserContext'
|
||||
|
@ -20,6 +21,8 @@ const remove = className => {
|
|||
const intervalMS = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
function App() {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
startOpenReplay()
|
||||
|
||||
const { mode, systemMode } = useColorScheme()
|
||||
|
@ -92,6 +95,7 @@ function App() {
|
|||
|
||||
return (
|
||||
<div className='min-h-screen'>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthenticationProvider />
|
||||
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
||||
<NavBar />
|
||||
|
@ -114,6 +118,8 @@ function App() {
|
|||
</Button>
|
||||
</Snackbar>
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
,
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import ChoreView from '../views/ChoreEdit/ChoreView'
|
|||
import MyChores from '../views/Chores/MyChores'
|
||||
import JoinCircleView from '../views/Circles/JoinCircle'
|
||||
import ChoreHistory from '../views/History/ChoreHistory'
|
||||
import LabelView from '../views/Labels/LabelView'
|
||||
import Landing from '../views/Landing/Landing'
|
||||
import PaymentCancelledView from '../views/Payments/PaymentFailView'
|
||||
import PaymentSuccessView from '../views/Payments/PaymentSuccessView'
|
||||
|
@ -116,6 +117,10 @@ const Router = createBrowserRouter([
|
|||
path: 'things/:id',
|
||||
element: <ThingsHistory />,
|
||||
},
|
||||
{
|
||||
path: 'labels/',
|
||||
element: <LabelView />,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
|
17
src/queries/ChoreQueries.jsx
Normal file
17
src/queries/ChoreQueries.jsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useQuery } from 'react-query'
|
||||
import { CreateChore, GetChoresNew } from '../utils/Fetcher'
|
||||
|
||||
export const useChores = () => {
|
||||
return useQuery('chores', GetChoresNew)
|
||||
}
|
||||
|
||||
export const useCreateChore = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation(CreateChore, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries('chores')
|
||||
},
|
||||
})
|
||||
}
|
6
src/queries/UserQueries.jsx
Normal file
6
src/queries/UserQueries.jsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { useQuery } from 'react-query'
|
||||
import { GetAllUsers } from '../utils/Fetcher'
|
||||
|
||||
export const useAllUsers = () => {
|
||||
return useQuery('allUsers', GetAllUsers)
|
||||
}
|
|
@ -45,6 +45,13 @@ const GetAllUsers = () => {
|
|||
headers: HEADERS(),
|
||||
})
|
||||
}
|
||||
const GetChoresNew = async () => {
|
||||
const resp = await Fetch(`${API_URL}/chores/`, {
|
||||
method: 'GET',
|
||||
headers: HEADERS(),
|
||||
})
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
const GetChores = () => {
|
||||
return Fetch(`${API_URL}/chores/`, {
|
||||
|
@ -294,16 +301,49 @@ const GetLongLiveTokens = () => {
|
|||
headers: HEADERS(),
|
||||
})
|
||||
}
|
||||
|
||||
const CreateLabel = label => {
|
||||
return Fetch(`${API_URL}/labels`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS(),
|
||||
body: JSON.stringify(label),
|
||||
})
|
||||
}
|
||||
|
||||
const GetLabels = async () => {
|
||||
const resp = await Fetch(`${API_URL}/labels`, {
|
||||
method: 'GET',
|
||||
headers: HEADERS(),
|
||||
})
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
const UpdateLabel = label => {
|
||||
return Fetch(`${API_URL}/labels`, {
|
||||
method: 'PUT',
|
||||
headers: HEADERS(),
|
||||
body: JSON.stringify(label),
|
||||
})
|
||||
}
|
||||
const DeleteLabel = id => {
|
||||
return Fetch(`${API_URL}/labels/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: HEADERS(),
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
AcceptCircleMemberRequest,
|
||||
CancelSubscription,
|
||||
createChore,
|
||||
CreateChore,
|
||||
CreateLabel,
|
||||
CreateLongLiveToken,
|
||||
CreateThing,
|
||||
DeleteChore,
|
||||
DeleteChoreHistory,
|
||||
DeleteCircleMember,
|
||||
DeleteLabel,
|
||||
DeleteLongLiveToken,
|
||||
DeleteThing,
|
||||
GetAllCircleMembers,
|
||||
|
@ -312,7 +352,9 @@ export {
|
|||
GetChoreDetailById,
|
||||
GetChoreHistory,
|
||||
GetChores,
|
||||
GetChoresNew,
|
||||
GetCircleMemberRequests,
|
||||
GetLabels,
|
||||
GetLongLiveTokens,
|
||||
GetSubscriptionSession,
|
||||
GetThingHistory,
|
||||
|
@ -330,6 +372,7 @@ export {
|
|||
UpdateChoreAssignee,
|
||||
UpdateChoreHistory,
|
||||
UpdateChorePriority,
|
||||
UpdateLabel,
|
||||
UpdatePassword,
|
||||
UpdateThingState,
|
||||
UpdateUserDetails,
|
||||
|
|
38
src/utils/LabelColors.jsx
Normal file
38
src/utils/LabelColors.jsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
const LABEL_COLORS = [
|
||||
{ name: 'Default', value: '#FFFFFF' },
|
||||
{ name: 'Salmon', value: '#ff7961' },
|
||||
{ name: 'Teal', value: '#26a69a' },
|
||||
{ name: 'Sky Blue', value: '#80d8ff' },
|
||||
{ name: 'Grape', value: '#7e57c2' },
|
||||
{ name: 'Sunshine', value: '#ffee58' },
|
||||
{ name: 'Coral', value: '#ff7043' },
|
||||
{ name: 'Lavender', value: '#ce93d8' },
|
||||
{ name: 'Rose', value: '#f48fb1' },
|
||||
{ name: 'Charcoal', value: '#616161' },
|
||||
{ name: 'Sienna', value: '#8d6e63' },
|
||||
{ name: 'Mint', value: '#a7ffeb' },
|
||||
{ name: 'Amber', value: '#ffc107' },
|
||||
{ name: 'Cobalt', value: '#3f51b5' },
|
||||
{ name: 'Emerald', value: '#4caf50' },
|
||||
{ name: 'Peach', value: '#ffab91' },
|
||||
{ name: 'Ocean', value: '#0288d1' },
|
||||
{ name: 'Mustard', value: '#ffca28' },
|
||||
{ name: 'Ruby', value: '#d32f2f' },
|
||||
{ name: 'Periwinkle', value: '#b39ddb' },
|
||||
{ name: 'Turquoise', value: '#00bcd4' },
|
||||
{ name: 'Lime', value: '#cddc39' },
|
||||
{ name: 'Blush', value: '#f8bbd0' },
|
||||
{ name: 'Ash', value: '#90a4ae' },
|
||||
{ name: 'Sand', value: '#d7ccc8' },
|
||||
]
|
||||
|
||||
export default LABEL_COLORS
|
||||
|
||||
export const getTextColorFromBackgroundColor = bgColor => {
|
||||
if (!bgColor) return ''
|
||||
const hex = bgColor.replace('#', '')
|
||||
const r = parseInt(hex.substring(0, 2), 16)
|
||||
const g = parseInt(hex.substring(2, 4), 16)
|
||||
const b = parseInt(hex.substring(4, 6), 16)
|
||||
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#000000' : '#ffffff'
|
||||
}
|
|
@ -10,21 +10,25 @@ const Priorities = [
|
|||
name: 'P4',
|
||||
value: 4,
|
||||
icon: <HorizontalRule />,
|
||||
color: '',
|
||||
},
|
||||
{
|
||||
name: 'P3 ',
|
||||
value: 3,
|
||||
icon: <KeyboardControlKey />,
|
||||
color: '',
|
||||
},
|
||||
{
|
||||
name: 'P2',
|
||||
value: 2,
|
||||
icon: <KeyboardDoubleArrowUp />,
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
name: 'P1',
|
||||
value: 1,
|
||||
icon: <PriorityHigh />,
|
||||
color: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
@ -67,6 +71,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)
|
||||
|
@ -83,6 +88,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 = () => {
|
||||
|
@ -173,7 +181,8 @@ const ChoreEdit = () => {
|
|||
isRolling: isRolling,
|
||||
isActive: isActive,
|
||||
notification: isNotificable,
|
||||
labels: labels,
|
||||
labels: labels.map(l => l.name),
|
||||
labelsV2: labelsV2,
|
||||
notificationMetadata: notificationMetadata,
|
||||
thingTrigger: thingTrigger,
|
||||
}
|
||||
|
@ -227,8 +236,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
|
||||
|
@ -276,6 +286,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) {
|
||||
|
@ -330,6 +348,7 @@ const ChoreEdit = () => {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth='md'>
|
||||
{/* <Typography level='h3' mb={1.5}>
|
||||
|
@ -727,8 +746,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 => {
|
||||
|
@ -742,7 +762,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 }}>
|
||||
|
@ -823,6 +935,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
|
||||
|
@ -330,16 +347,26 @@ const ChoreView = () => {
|
|||
<Menu>
|
||||
{Priorities.map((priority, index) => (
|
||||
<MenuItem
|
||||
sx={{
|
||||
pr: 1,
|
||||
py: 1,
|
||||
}}
|
||||
key={index}
|
||||
onClick={() => {
|
||||
handleUpdatePriority(priority)
|
||||
}}
|
||||
color={priority.color}
|
||||
>
|
||||
{priority.icon}
|
||||
{priority.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
sx={{
|
||||
pr: 1,
|
||||
py: 1,
|
||||
}}
|
||||
onClick={() => {
|
||||
handleUpdatePriority({
|
||||
name: 'No Priority',
|
||||
|
|
|
@ -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) => (
|
||||
{chore.labelsV2?.map((l, index) => {
|
||||
return (
|
||||
<Chip
|
||||
variant='solid'
|
||||
key={label}
|
||||
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)}
|
||||
|
||||
// startDecorator={getIconForLabel(label)}
|
||||
>
|
||||
{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
|
|
@ -70,7 +70,6 @@ const ThingCard = ({
|
|||
// flexDirection: 'row', // Change to 'row'
|
||||
justifyContent: 'space-between',
|
||||
p: 2,
|
||||
backgroundColor: 'white',
|
||||
boxShadow: 'sm',
|
||||
borderRadius: 8,
|
||||
mb: 1,
|
||||
|
|
|
@ -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
Reference in a new issue