Add SmartTaskInput with Custom Renderer.
Change to use `@tanstack/react-query` Add Global ErrorProvider Add limited Support for offline version Fix issue with calendar display +1 day
This commit is contained in:
165
package-lock.json
generated
165
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "donetick",
|
"name": "donetick",
|
||||||
"version": "0.1.98",
|
"version": "0.1.99",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "donetick",
|
"name": "donetick",
|
||||||
"version": "0.1.98",
|
"version": "0.1.99",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/sqlite": "^6.0.2",
|
"@capacitor-community/sqlite": "^6.0.2",
|
||||||
"@capacitor/android": "^6.1.1",
|
"@capacitor/android": "^6.1.1",
|
||||||
@@ -42,7 +42,6 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-calendar": "^5.1.0",
|
"react-calendar": "^5.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-query": "^3.39.3",
|
|
||||||
"react-router-dom": "^6.21.1",
|
"react-router-dom": "^6.21.1",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"reactjs-social-login": "^2.6.3",
|
"reactjs-social-login": "^2.6.3",
|
||||||
@@ -5228,81 +5227,6 @@
|
|||||||
"node": ">=8"
|
"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/broadcast-channel/node_modules/brace-expansion": {
|
|
||||||
"version": "1.1.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0",
|
|
||||||
"concat-map": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/broadcast-channel/node_modules/glob": {
|
|
||||||
"version": "7.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
|
||||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
|
||||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"fs.realpath": "^1.0.0",
|
|
||||||
"inflight": "^1.0.4",
|
|
||||||
"inherits": "2",
|
|
||||||
"minimatch": "^3.1.1",
|
|
||||||
"once": "^1.3.0",
|
|
||||||
"path-is-absolute": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/broadcast-channel/node_modules/minimatch": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^1.1.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/broadcast-channel/node_modules/rimraf": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
|
||||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"glob": "^7.1.3"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"rimraf": "bin.js"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/browser-fs-access": {
|
"node_modules/browser-fs-access": {
|
||||||
"version": "0.35.0",
|
"version": "0.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz",
|
"resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz",
|
||||||
@@ -6566,12 +6490,6 @@
|
|||||||
"node": ">=8"
|
"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/dezalgo": {
|
"node_modules/dezalgo": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||||
@@ -9341,12 +9259,6 @@
|
|||||||
"node": ">=14"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -9807,16 +9719,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -10054,12 +9956,6 @@
|
|||||||
"node": ">=8.6"
|
"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-fn": {
|
"node_modules/mimic-fn": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
||||||
@@ -10226,15 +10122,6 @@
|
|||||||
"thenify-all": "^1.0.0"
|
"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": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.8",
|
"version": "3.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||||
@@ -10542,12 +10429,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@@ -11409,32 +11290,6 @@
|
|||||||
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
|
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"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": {
|
"node_modules/react-router": {
|
||||||
"version": "6.28.1",
|
"version": "6.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.1.tgz",
|
||||||
@@ -11882,12 +11737,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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/replace": {
|
"node_modules/replace": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/replace/-/replace-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/replace/-/replace-1.2.2.tgz",
|
||||||
@@ -13855,16 +13704,6 @@
|
|||||||
"node": ">= 10.0.0"
|
"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/untildify": {
|
"node_modules/untildify": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
||||||
|
|||||||
13
src/App.jsx
13
src/App.jsx
@@ -1,13 +1,14 @@
|
|||||||
import NavBar from '@/views/components/NavBar'
|
import NavBar from '@/views/components/NavBar'
|
||||||
import { Button, Snackbar, Typography, useColorScheme } from '@mui/joy'
|
import { Button, Snackbar, Typography, useColorScheme } from '@mui/joy'
|
||||||
import Tracker from '@openreplay/tracker'
|
import Tracker from '@openreplay/tracker'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
|
||||||
import { Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||||
import { registerCapacitorListeners } from './CapacitorListener'
|
import { registerCapacitorListeners } from './CapacitorListener'
|
||||||
import { UserContext } from './contexts/UserContext'
|
import { UserContext } from './contexts/UserContext'
|
||||||
import { AuthenticationProvider } from './service/AuthenticationService'
|
import { AuthenticationProvider } from './service/AuthenticationService'
|
||||||
|
import { ErrorProvider } from './service/ErrorProvider'
|
||||||
import { GetUserProfile } from './utils/Fetcher'
|
import { GetUserProfile } from './utils/Fetcher'
|
||||||
import { apiManager, isTokenValid } from './utils/TokenManager'
|
import { apiManager, isTokenValid } from './utils/TokenManager'
|
||||||
import NetworkBanner from './views/components/NetworkBanner'
|
import NetworkBanner from './views/components/NetworkBanner'
|
||||||
@@ -91,10 +92,12 @@ function App() {
|
|||||||
<NetworkBanner />
|
<NetworkBanner />
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthenticationProvider />
|
<AuthenticationProvider />
|
||||||
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
<ErrorProvider>
|
||||||
<NavBar />
|
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
||||||
<Outlet />
|
<NavBar />
|
||||||
</UserContext.Provider>
|
<Outlet />
|
||||||
|
</UserContext.Provider>
|
||||||
|
</ErrorProvider>
|
||||||
{needRefresh && (
|
{needRefresh && (
|
||||||
<Snackbar open={showUpdateSnackbar}>
|
<Snackbar open={showUpdateSnackbar}>
|
||||||
<Typography level='body-md'>
|
<Typography level='body-md'>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Network } from '@capacitor/network'
|
import { Network } from '@capacitor/network'
|
||||||
// import { localStore } from '../utils/LocalStore'
|
import { localStore } from '../utils/LocalStore'
|
||||||
import { syncManager } from '../utils/SyncManager.jsx' // Ensure you import syncManager if needed for syncing
|
import { syncManager } from '../utils/SyncManager.jsx' // Ensure you import syncManager if needed for syncing
|
||||||
class NetworkManager {
|
class NetworkManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -23,29 +23,33 @@ class NetworkManager {
|
|||||||
this.isOnline = status.connected
|
this.isOnline = status.connected
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const syncQueue = () => {
|
||||||
|
localStore
|
||||||
|
.syncQueuedRequests()
|
||||||
|
.then(hasMessages => {
|
||||||
|
console.log(
|
||||||
|
'Queued requests synced successfully. Queue has messaage is: ',
|
||||||
|
hasMessages,
|
||||||
|
)
|
||||||
|
if (hasMessages) {
|
||||||
|
this.notifyBackendSync()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error syncing queued requests:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
this.registerNetworkListener(async isOnline => {
|
this.registerNetworkListener(async isOnline => {
|
||||||
if (isOnline && this.isNetworkOn) {
|
if (isOnline && this.isNetworkOn) {
|
||||||
// TODO: Delete when Sync manager Implemented
|
// TODO: Delete when Sync manager Implemented
|
||||||
// localStore
|
syncQueue()
|
||||||
// .syncQueuedRequests()
|
|
||||||
// .then(hasMessages => {
|
|
||||||
// console.log(
|
|
||||||
// 'Queued requests synced successfully. Queue has messaage is: ',
|
|
||||||
// hasMessages,
|
|
||||||
// )
|
|
||||||
// if (hasMessages) {
|
|
||||||
// this.notifyBackendSync()
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .catch(error => {
|
|
||||||
// console.error('Error syncing queued requests:', error)
|
|
||||||
// })
|
|
||||||
console.log('NetworkManager: Network is back online. with SYNCMANAGER')
|
console.log('NetworkManager: Network is back online. with SYNCMANAGER')
|
||||||
|
|
||||||
await syncManager.syncTasks()
|
await syncManager.syncTasks()
|
||||||
console.log('Finished syncing queued requests.')
|
console.log('Finished syncing queued requests.')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
syncQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
setOffline() {
|
setOffline() {
|
||||||
|
|||||||
@@ -67,19 +67,22 @@ export const useCreateChore = () => {
|
|||||||
{ ...newTask, id: tempId, tempId }, // Use the tempId for offline tracking
|
{ ...newTask, id: tempId, tempId }, // Use the tempId for offline tracking
|
||||||
]
|
]
|
||||||
await localStore.saveToCache('offlineTasks', updateOfflineTasks) // Save to local storage
|
await localStore.saveToCache('offlineTasks', updateOfflineTasks) // Save to local storage
|
||||||
|
// force useChores to refetch:
|
||||||
|
queryClient.invalidateQueries(['chores'])
|
||||||
|
// Force the chores query to refetch
|
||||||
|
queryClient.refetchQueries(['chores'])
|
||||||
// Update the chores query cache immediately
|
// Update the chores query cache immediately
|
||||||
queryClient.setQueryData(['chores'], oldData => {
|
// queryClient.setQueryData(['chores'], oldData => {
|
||||||
console.log('ATTEMPT TO SAVE OFFLINE TASKS:', updateOfflineTasks)
|
// console.log('ATTEMPT TO SAVE OFFLINE TASKS:', updateOfflineTasks)
|
||||||
|
|
||||||
if (!oldData)
|
// if (!oldData)
|
||||||
return {
|
// return {
|
||||||
res: [{ ...newTask, id: tempId, tempId }],
|
// res: [{ ...newTask, id: tempId, tempId }],
|
||||||
} // If no data, return offline tasks
|
// } // If no data, return offline tasks
|
||||||
return {
|
// return {
|
||||||
res: [...oldData.res, { ...newTask, id: tempId, tempId }],
|
// res: [...oldData.res, { ...newTask, id: tempId, tempId }],
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
return { tempId }
|
return { tempId }
|
||||||
}
|
}
|
||||||
return { tempId: null }
|
return { tempId: null }
|
||||||
@@ -135,11 +138,11 @@ export const useUpdateChore = () => {
|
|||||||
throw new Error('Failed to save chore')
|
throw new Error('Failed to save chore')
|
||||||
}
|
}
|
||||||
const updatedChoreRes = await resp.json()
|
const updatedChoreRes = await resp.json()
|
||||||
if (!updatedChoreRes || !updatedChoreRes.res) {
|
if (!updatedChoreRes) {
|
||||||
throw new Error('Failed to get updated chore data')
|
throw new Error('Failed to get updated chore data')
|
||||||
}
|
}
|
||||||
// Successfully updated the chore on the server, return the updated chore
|
// Successfully updated the chore on the server, return the updated chore
|
||||||
return updatedChoreRes.res
|
return updatedChoreRes?.res || updatedChoreRes
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
@@ -158,9 +161,13 @@ export const useUpdateChore = () => {
|
|||||||
export const useChoresHistory = (initialLimit, includeMembers) => {
|
export const useChoresHistory = (initialLimit, includeMembers) => {
|
||||||
const [limit, setLimit] = useState(initialLimit) // Initially, no limit is selected
|
const [limit, setLimit] = useState(initialLimit) // Initially, no limit is selected
|
||||||
|
|
||||||
const { data, error, isLoading } = useQuery(['choresHistory', limit], () =>
|
const { data, error, isLoading } = useQuery({
|
||||||
GetChoresHistory(limit, includeMembers),
|
queryKey: ['choresHistory', limit],
|
||||||
)
|
queryFn: async () => {
|
||||||
|
const resp = await GetChoresHistory(limit, includeMembers)
|
||||||
|
return resp?.res || []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const handleLimitChange = newLimit => {
|
const handleLimitChange = newLimit => {
|
||||||
setLimit(newLimit)
|
setLimit(newLimit)
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { useQuery } from 'react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { GetResource } from '../utils/Fetcher'
|
import { GetResource } from '../utils/Fetcher'
|
||||||
|
|
||||||
export const useResource = () => {
|
export const useResource = () => {
|
||||||
const { data, isLoading, error } = useQuery('resource', GetResource, {
|
const { data, isLoading, error } = useQuery({
|
||||||
cacheTime: 1000 * 60 * 10, // 1 minute
|
queryKey: ['resource'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await GetResource()
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
})
|
})
|
||||||
return { data, isLoading, error }
|
return { data, isLoading, error }
|
||||||
}
|
}
|
||||||
|
|||||||
94
src/queries/SubtaskQueries.jsx
Normal file
94
src/queries/SubtaskQueries.jsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { networkManager } from '../hooks/NetworkManager'
|
||||||
|
import { CompleteSubTask, SaveChore } from '../utils/Fetcher'
|
||||||
|
import { localStore } from '../utils/LocalStore'
|
||||||
|
|
||||||
|
export const useUpdate = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async updatedChore => {
|
||||||
|
if (!networkManager.isOnline) {
|
||||||
|
updatedChore['updatedAt'] = new Date().toISOString()
|
||||||
|
if (!updatedChore['nextDueDate']) {
|
||||||
|
updatedChore['nextDueDate'] = updatedChore['dueDate']
|
||||||
|
}
|
||||||
|
const offlineTasks =
|
||||||
|
(await localStore.getFromCache('offlineTasks')) || []
|
||||||
|
|
||||||
|
for (const task of offlineTasks) {
|
||||||
|
// Find the task with the same id or tempId and update it
|
||||||
|
if (task.id === updatedChore.id || task.tempId === updatedChore.id) {
|
||||||
|
// Update the task in local storage
|
||||||
|
const updatedTask = { ...task, ...updatedChore }
|
||||||
|
const updatedOfflineTasks = offlineTasks.map(t =>
|
||||||
|
t.id === task.id ? updatedTask : t,
|
||||||
|
)
|
||||||
|
await localStore.saveToCache('offlineTasks', updatedOfflineTasks)
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
resolve(updatedTask)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newTaskId = crypto.randomUUID()
|
||||||
|
const updatedChoreWithNewId = {
|
||||||
|
...updatedChore,
|
||||||
|
tempId: newTaskId,
|
||||||
|
}
|
||||||
|
|
||||||
|
await localStore.saveToCache('offlineTasks', [
|
||||||
|
...offlineTasks,
|
||||||
|
updatedChoreWithNewId,
|
||||||
|
])
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Resolve with the updated task
|
||||||
|
resolve(updatedChoreWithNewId)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Call the API to update the chore
|
||||||
|
const resp = await SaveChore(updatedChore)
|
||||||
|
if (!resp || !resp.ok) {
|
||||||
|
throw new Error('Failed to save chore')
|
||||||
|
}
|
||||||
|
const updatedChoreRes = await resp.json()
|
||||||
|
if (!updatedChoreRes) {
|
||||||
|
throw new Error('Failed to get updated chore data')
|
||||||
|
}
|
||||||
|
// Successfully updated the chore on the server, return the updated chore
|
||||||
|
return updatedChoreRes?.res || updatedChoreRes
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
// Invalidate the chores query to refresh the data
|
||||||
|
queryClient.invalidateQueries(['chores'])
|
||||||
|
},
|
||||||
|
onMutate: async updatedChore => {
|
||||||
|
if (!networkManager.isOnline) {
|
||||||
|
// Handle offline case here if needed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCompleteSubTask = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (subTaskId, choreId, completedAt) => {
|
||||||
|
if (!networkManager.isOnline) {
|
||||||
|
throw new Error('Cannot complete subtask while offline')
|
||||||
|
}
|
||||||
|
const resp = await CompleteSubTask(subTaskId, choreId, completedAt)
|
||||||
|
if (!resp || !resp.ok) {
|
||||||
|
throw new Error('Failed to complete subtask')
|
||||||
|
}
|
||||||
|
const result = await resp.json()
|
||||||
|
if (!result || !result.res) {
|
||||||
|
throw new Error('Failed to get completed subtask data')
|
||||||
|
}
|
||||||
|
return result.res
|
||||||
|
},
|
||||||
|
onSuccess: (data, variables) => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery } from 'react-query'
|
|
||||||
import { GetAllCircleMembers, GetAllUsers } from '../utils/Fetcher'
|
import { GetAllCircleMembers, GetAllUsers } from '../utils/Fetcher'
|
||||||
|
|
||||||
export const useAllUsers = () => {
|
export const useAllUsers = () => {
|
||||||
return useQuery('allUsers', GetAllUsers)
|
return useQuery({
|
||||||
|
queryKey: ['allUsers'],
|
||||||
|
queryFn: GetAllUsers,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCircleMembers = () => {
|
export const useCircleMembers = () => {
|
||||||
const [refetchKey, setRefetchKey] = useState(0)
|
const [refetchKey, setRefetchKey] = useState(0)
|
||||||
|
|
||||||
const { data, error, isLoading, refetch } = useQuery(
|
const { data, error, isLoading, refetch } = useQuery({
|
||||||
['allCircleMembers', refetchKey],
|
queryKey: ['allCircleMembers', refetchKey],
|
||||||
GetAllCircleMembers,
|
queryFn: GetAllCircleMembers,
|
||||||
)
|
})
|
||||||
const handleRefetch = () => {
|
const handleRefetch = () => {
|
||||||
setRefetchKey(prevKey => prevKey + 1)
|
setRefetchKey(prevKey => prevKey + 1)
|
||||||
refetch()
|
refetch()
|
||||||
|
|||||||
51
src/service/ErrorProvider.jsx
Normal file
51
src/service/ErrorProvider.jsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Error } from '@mui/icons-material'
|
||||||
|
import { Box, Button, Snackbar, Typography } from '@mui/joy'
|
||||||
|
import React, { createContext, useContext, useState } from 'react'
|
||||||
|
|
||||||
|
const ErrorContext = createContext()
|
||||||
|
|
||||||
|
export const useError = () => useContext(ErrorContext)
|
||||||
|
|
||||||
|
export const ErrorProvider = ({ children }) => {
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
|
||||||
|
const showError = error => {
|
||||||
|
setError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorContext.Provider value={{ showError }}>
|
||||||
|
{children}
|
||||||
|
<Snackbar
|
||||||
|
open={Boolean(error)}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={() => setError(null)}
|
||||||
|
startDecorator={<Error color='danger' />}
|
||||||
|
endDecorator={
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
color='danger'
|
||||||
|
onClick={() => setError(null)}
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{typeof error === 'string' ? (
|
||||||
|
<Typography color='danger' level='body-md'>
|
||||||
|
{error}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
<Typography color='danger' level='title-sm'>
|
||||||
|
{error?.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='danger' level='body-sm'>
|
||||||
|
{error?.message}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Snackbar>
|
||||||
|
</ErrorContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { CapacitorSQLite } from '@capacitor-community/sqlite'
|
import { CapacitorSQLite } from '@capacitor-community/sqlite'
|
||||||
import { Capacitor } from '@capacitor/core'
|
|
||||||
|
|
||||||
const CACHE_TABLE = 'offline_cache'
|
const CACHE_TABLE = 'offline_cache'
|
||||||
const QUEUE_TABLE = 'offline_request_queue'
|
const QUEUE_TABLE = 'offline_request_queue'
|
||||||
@@ -8,7 +7,8 @@ const OFFLINE_TASK = 'offlineTasks' // For storing offline tasks
|
|||||||
class LocalStore {
|
class LocalStore {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.db = null
|
this.db = null
|
||||||
this.useLocalStorage = !Capacitor.isNativePlatform()
|
// this.useLocalStorage = !Capacitor.isNativePlatform()
|
||||||
|
this.useLocalStorage = true // default to localStorage for now.
|
||||||
}
|
}
|
||||||
|
|
||||||
async initDatabase() {
|
async initDatabase() {
|
||||||
@@ -153,10 +153,15 @@ class LocalStore {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async queueRequest(requestId, requestBody) {
|
async queueRequest(requestId, requestPayload) {
|
||||||
if (this.useLocalStorage) {
|
if (this.useLocalStorage) {
|
||||||
const queue = JSON.parse(localStorage.getItem(QUEUE_TABLE)) || []
|
const queue = JSON.parse(localStorage.getItem(QUEUE_TABLE)) || []
|
||||||
queue.push({ requestId, requestBody })
|
console.log('requestPayload', requestPayload)
|
||||||
|
|
||||||
|
if (typeof requestPayload?.options?.body['id'] === 'string') {
|
||||||
|
requestPayload['id'] = null
|
||||||
|
}
|
||||||
|
queue.push({ requestId, requestBody: requestPayload })
|
||||||
localStorage.setItem(QUEUE_TABLE, JSON.stringify(queue))
|
localStorage.setItem(QUEUE_TABLE, JSON.stringify(queue))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -167,11 +172,13 @@ class LocalStore {
|
|||||||
INSERT INTO ${QUEUE_TABLE} (url, requestBody)
|
INSERT INTO ${QUEUE_TABLE} (url, requestBody)
|
||||||
VALUES (?, ?);
|
VALUES (?, ?);
|
||||||
`,
|
`,
|
||||||
[requestId, JSON.stringify(requestBody)],
|
[requestId, JSON.stringify(requestPayload)],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncQueuedRequests() {
|
async syncQueuedRequests() {
|
||||||
|
console.log('Syncing queued requests...')
|
||||||
|
|
||||||
var queueSize = 0
|
var queueSize = 0
|
||||||
if (this.useLocalStorage) {
|
if (this.useLocalStorage) {
|
||||||
const queue = JSON.parse(localStorage.getItem(QUEUE_TABLE)) || []
|
const queue = JSON.parse(localStorage.getItem(QUEUE_TABLE)) || []
|
||||||
|
|||||||
@@ -137,7 +137,11 @@ async function handleOfflineRequest(url, options) {
|
|||||||
const requestId = murmurhash.v3(JSON.stringify({ url, options }))
|
const requestId = murmurhash.v3(JSON.stringify({ url, options }))
|
||||||
await localStore.queueRequest(requestId, { url, options })
|
await localStore.queueRequest(requestId, { url, options })
|
||||||
console.log('Request queued for later processing:', requestId)
|
console.log('Request queued for later processing:', requestId)
|
||||||
return Promise.reject(new Error('Offline and request queued: ' + requestId))
|
return Promise.reject({
|
||||||
|
error: 'Offline and request queued',
|
||||||
|
requestId,
|
||||||
|
queued: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function attemptFetchFromCache(url, options) {
|
async function attemptFetchFromCache(url, options) {
|
||||||
@@ -152,6 +156,7 @@ async function attemptFetchFromCache(url, options) {
|
|||||||
json: async () => cachedData,
|
json: async () => cachedData,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: change this to throw error instead of returning promise
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new Error(
|
new Error(
|
||||||
'No cached data found for URL: ' +
|
'No cached data found for URL: ' +
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ const ChoreEdit = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (isChoreLoading && choreId) {
|
if ((isChoreLoading && choreId) || isUserLabelsLoading) {
|
||||||
return <LoadingComponent />
|
return <LoadingComponent />
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -573,7 +573,6 @@ const ChoreEdit = () => {
|
|||||||
}}
|
}}
|
||||||
defaultChecked={dueDate !== null}
|
defaultChecked={dueDate !== null}
|
||||||
checked={dueDate !== null}
|
checked={dueDate !== null}
|
||||||
value={dueDate !== null}
|
|
||||||
overlay
|
overlay
|
||||||
label='Give this task a due date'
|
label='Give this task a due date'
|
||||||
/>
|
/>
|
||||||
@@ -711,7 +710,6 @@ const ChoreEdit = () => {
|
|||||||
}}
|
}}
|
||||||
defaultChecked={isNotificable}
|
defaultChecked={isNotificable}
|
||||||
checked={isNotificable}
|
checked={isNotificable}
|
||||||
value={isNotificable}
|
|
||||||
disabled={!isPlusAccount(userProfile)}
|
disabled={!isPlusAccount(userProfile)}
|
||||||
overlay
|
overlay
|
||||||
label='Notify for this task'
|
label='Notify for this task'
|
||||||
@@ -967,7 +965,6 @@ const ChoreEdit = () => {
|
|||||||
}}
|
}}
|
||||||
overlay
|
overlay
|
||||||
checked={subTasks != null}
|
checked={subTasks != null}
|
||||||
value={subTasks != null}
|
|
||||||
label='Sub Tasks'
|
label='Sub Tasks'
|
||||||
/>
|
/>
|
||||||
<FormHelperText>Add sub tasks to this task</FormHelperText>
|
<FormHelperText>Add sub tasks to this task</FormHelperText>
|
||||||
@@ -997,7 +994,6 @@ const ChoreEdit = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
checked={points > -1}
|
checked={points > -1}
|
||||||
value={points > -1}
|
|
||||||
overlay
|
overlay
|
||||||
label='Assign Points'
|
label='Assign Points'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ const ChoreView = () => {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
mb: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -260,21 +261,31 @@ const ChoreView = () => {
|
|||||||
? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}`
|
? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}`
|
||||||
: 'N/A'}
|
: 'N/A'}
|
||||||
</Chip>
|
</Chip>
|
||||||
{chore?.labelsV2?.map((label, index) => (
|
<Box
|
||||||
<Chip
|
sx={{
|
||||||
key={index}
|
display: 'flex',
|
||||||
sx={{
|
flexDirection: 'row',
|
||||||
position: 'relative',
|
justifyContent: 'center',
|
||||||
ml: index === 0 ? 0 : 0.5,
|
alignItems: 'center',
|
||||||
top: 2,
|
mb: 1,
|
||||||
zIndex: 1,
|
}}
|
||||||
backgroundColor: label?.color,
|
>
|
||||||
color: getTextColorFromBackgroundColor(label?.color),
|
{chore?.labelsV2?.map((label, index) => (
|
||||||
}}
|
<Chip
|
||||||
>
|
key={index}
|
||||||
{label?.name}
|
sx={{
|
||||||
</Chip>
|
position: 'relative',
|
||||||
))}
|
ml: index === 0 ? 0 : 0.5,
|
||||||
|
top: 2,
|
||||||
|
zIndex: 1,
|
||||||
|
backgroundColor: label?.color,
|
||||||
|
color: getTextColorFromBackgroundColor(label?.color),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label?.name}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import moment from 'moment'
|
|||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { UserContext } from '../../contexts/UserContext'
|
import { UserContext } from '../../contexts/UserContext'
|
||||||
|
import { useError } from '../../service/ErrorProvider'
|
||||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||||
import {
|
import {
|
||||||
@@ -91,6 +92,7 @@ const ChoreCard = ({
|
|||||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
||||||
const [timeoutId, setTimeoutId] = React.useState(null)
|
const [timeoutId, setTimeoutId] = React.useState(null)
|
||||||
const { userProfile } = React.useContext(UserContext)
|
const { userProfile } = React.useContext(UserContext)
|
||||||
|
const { showError } = useError()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('mousedown', handleMenuOutsideClick)
|
document.addEventListener('mousedown', handleMenuOutsideClick)
|
||||||
return () => {
|
return () => {
|
||||||
@@ -176,6 +178,7 @@ const ChoreCard = ({
|
|||||||
|
|
||||||
if (seconds <= 0) {
|
if (seconds <= 0) {
|
||||||
clearInterval(countdownInterval) // Stop the countdown when it reaches 0
|
clearInterval(countdownInterval) // Stop the countdown when it reaches 0
|
||||||
|
setIsPendingCompletion(false) // Reset the state
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
@@ -195,6 +198,25 @@ const ChoreCard = ({
|
|||||||
setTimeoutId(null)
|
setTimeoutId(null)
|
||||||
setSecondsLeftToCancel(null)
|
setSecondsLeftToCancel(null)
|
||||||
})
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error?.queued) {
|
||||||
|
showError({
|
||||||
|
title: 'Update Failed',
|
||||||
|
message: 'Request will be reattempt when you are online',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showError({
|
||||||
|
title: 'Failed to update',
|
||||||
|
message: error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsPendingCompletion(false)
|
||||||
|
clearTimeout(id)
|
||||||
|
clearInterval(countdownInterval) // Ensure to clear this interval as well
|
||||||
|
setTimeoutId(null)
|
||||||
|
setSecondsLeftToCancel(null)
|
||||||
|
})
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
setTimeoutId(id)
|
setTimeoutId(id)
|
||||||
@@ -676,15 +698,30 @@ const ChoreCard = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
SkipChore(chore.id).then(response => {
|
SkipChore(chore.id)
|
||||||
if (response.ok) {
|
.then(response => {
|
||||||
response.json().then(data => {
|
if (response.ok) {
|
||||||
const newChore = data.res
|
response.json().then(data => {
|
||||||
onChoreUpdate(newChore, 'skipped')
|
const newChore = data.res
|
||||||
handleMenuClose()
|
onChoreUpdate(newChore, 'skipped')
|
||||||
})
|
handleMenuClose()
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error?.queued) {
|
||||||
|
showError({
|
||||||
|
title: 'Failed to update',
|
||||||
|
message:
|
||||||
|
'Request will be processed when you are online',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showError({
|
||||||
|
title: 'Failed to update',
|
||||||
|
message: error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SwitchAccessShortcut />
|
<SwitchAccessShortcut />
|
||||||
|
|||||||
35
src/views/Chores/CompactCard.jsx
Normal file
35
src/views/Chores/CompactCard.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Box, Checkbox, Typography } from '@mui/joy'
|
||||||
|
|
||||||
|
export const CompactCard = ({
|
||||||
|
chore,
|
||||||
|
performers,
|
||||||
|
onChoreUpdate,
|
||||||
|
onChoreRemove,
|
||||||
|
userLabels,
|
||||||
|
sx,
|
||||||
|
viewOnly,
|
||||||
|
onChipClick,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
// checked={!!task.completedAt}
|
||||||
|
// onChange={() => handleToggle(task.id)}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
textDecoration: chore.completedAt ? 'line-through' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chore.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
Add,
|
Add,
|
||||||
|
Bolt,
|
||||||
CancelRounded,
|
CancelRounded,
|
||||||
EditCalendar,
|
EditCalendar,
|
||||||
ExpandCircleDown,
|
ExpandCircleDown,
|
||||||
Grain,
|
Grain,
|
||||||
PriorityHigh,
|
PriorityHigh,
|
||||||
Search,
|
|
||||||
Sort,
|
Sort,
|
||||||
Style,
|
Style,
|
||||||
Unarchive,
|
Unarchive,
|
||||||
@@ -63,11 +63,11 @@ const MyChores = () => {
|
|||||||
const [filteredChores, setFilteredChores] = useState([])
|
const [filteredChores, setFilteredChores] = useState([])
|
||||||
const [searchFilter, setSearchFilter] = useState('All')
|
const [searchFilter, setSearchFilter] = useState('All')
|
||||||
const [choreSections, setChoreSections] = useState([])
|
const [choreSections, setChoreSections] = useState([])
|
||||||
const [activeTextField, setActiveTextField] = useState(
|
|
||||||
localStorage.getItem('activeTextField') || 'task',
|
const [showSearchFilter, setShowSearchFilter] = useState(false)
|
||||||
)
|
const [addTaskModalOpen, setAddTaskModalOpen] = useState(false)
|
||||||
const [taskInputFocus, setTaskInputFocus] = useState(0)
|
const [taskInputFocus, setTaskInputFocus] = useState(0)
|
||||||
const searchInputRef = useRef()
|
const searchInputRef = useRef(null)
|
||||||
const [searchInputFocus, setSearchInputFocus] = useState(0)
|
const [searchInputFocus, setSearchInputFocus] = useState(0)
|
||||||
const [selectedChoreSection, setSelectedChoreSection] = useState(
|
const [selectedChoreSection, setSelectedChoreSection] = useState(
|
||||||
localStorage.getItem('selectedChoreSection') || 'due_date',
|
localStorage.getItem('selectedChoreSection') || 'due_date',
|
||||||
@@ -84,7 +84,11 @@ const MyChores = () => {
|
|||||||
const menuRef = useRef(null)
|
const menuRef = useRef(null)
|
||||||
const Navigate = useNavigate()
|
const Navigate = useNavigate()
|
||||||
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
||||||
const { data: choresData, isLoading: choresLoading } = useChores()
|
const {
|
||||||
|
data: choresData,
|
||||||
|
isLoading: choresLoading,
|
||||||
|
refetch: refetchChores,
|
||||||
|
} = useChores()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([GetChores(), GetAllUsers(), GetUserProfile()]).then(
|
Promise.all([GetChores(), GetAllUsers(), GetUserProfile()]).then(
|
||||||
@@ -173,6 +177,21 @@ const MyChores = () => {
|
|||||||
searchInputRef.current.selectionEnd = searchInputRef.current.value?.length
|
searchInputRef.current.selectionEnd = searchInputRef.current.value?.length
|
||||||
}
|
}
|
||||||
}, [searchInputFocus])
|
}, [searchInputFocus])
|
||||||
|
|
||||||
|
// add listern to Control/Command + K to focus on search input
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = event => {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
|
||||||
|
event.preventDefault()
|
||||||
|
setAddTaskModalOpen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
const setSelectedChoreSectionWithCache = value => {
|
const setSelectedChoreSectionWithCache = value => {
|
||||||
setSelectedChoreSection(value)
|
setSelectedChoreSection(value)
|
||||||
localStorage.setItem('selectedChoreSection', value)
|
localStorage.setItem('selectedChoreSection', value)
|
||||||
@@ -185,10 +204,6 @@ const MyChores = () => {
|
|||||||
setSelectedChoreFilter(value)
|
setSelectedChoreFilter(value)
|
||||||
localStorage.setItem('selectedChoreFilter', value)
|
localStorage.setItem('selectedChoreFilter', value)
|
||||||
}
|
}
|
||||||
const setActiveTextFieldWithCache = value => {
|
|
||||||
setActiveTextField(value)
|
|
||||||
localStorage.setItem('activeTextField', value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateChores = newChore => {
|
const updateChores = newChore => {
|
||||||
const newChores = chores
|
const newChores = chores
|
||||||
@@ -381,60 +396,36 @@ const MyChores = () => {
|
|||||||
gap: 0.5,
|
gap: 0.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeTextField == 'task' && (
|
<Input
|
||||||
<TaskInput
|
ref={searchInputRef}
|
||||||
autoFocus={taskInputFocus}
|
placeholder='Search'
|
||||||
onChoreUpdate={updateChores}
|
value={searchTerm}
|
||||||
/>
|
onFocus={() => {
|
||||||
)}
|
setShowSearchFilter(true)
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
mb: 1,
|
||||||
|
borderRadius: 24,
|
||||||
|
height: 24,
|
||||||
|
borderColor: 'text.disabled',
|
||||||
|
padding: 1,
|
||||||
|
}}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
endDecorator={
|
||||||
|
searchTerm && (
|
||||||
|
<CancelRounded
|
||||||
|
onClick={() => {
|
||||||
|
setSearchTerm('')
|
||||||
|
setFilteredChores(chores)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{activeTextField == 'search' && (
|
{/* {activeTextField != 'search' && (
|
||||||
<Input
|
|
||||||
ref={searchInputRef}
|
|
||||||
autoFocus={searchInputFocus > 0}
|
|
||||||
placeholder='Search'
|
|
||||||
value={searchTerm}
|
|
||||||
fullWidth
|
|
||||||
sx={{
|
|
||||||
mt: 1,
|
|
||||||
mb: 1,
|
|
||||||
borderRadius: 24,
|
|
||||||
height: 24,
|
|
||||||
borderColor: 'text.disabled',
|
|
||||||
padding: 1,
|
|
||||||
}}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
endDecorator={
|
|
||||||
searchTerm && (
|
|
||||||
<CancelRounded
|
|
||||||
onClick={() => {
|
|
||||||
setSearchTerm('')
|
|
||||||
setFilteredChores(chores)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{activeTextField != 'task' && (
|
|
||||||
<IconButton
|
|
||||||
variant='outlined'
|
|
||||||
size='sm'
|
|
||||||
color='neutral'
|
|
||||||
sx={{
|
|
||||||
height: 24,
|
|
||||||
borderRadius: 24,
|
|
||||||
// minWidth: 100,
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
setActiveTextFieldWithCache('task')
|
|
||||||
setTaskInputFocus(taskInputFocus + 1)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditCalendar />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
{activeTextField != 'search' && (
|
|
||||||
<IconButton
|
<IconButton
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
color='neutral'
|
color='neutral'
|
||||||
@@ -452,7 +443,7 @@ const MyChores = () => {
|
|||||||
>
|
>
|
||||||
<Search />
|
<Search />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)} */}
|
||||||
<SortAndGrouping
|
<SortAndGrouping
|
||||||
title='Group by'
|
title='Group by'
|
||||||
k={'icon-menu-group-by'}
|
k={'icon-menu-group-by'}
|
||||||
@@ -496,7 +487,7 @@ const MyChores = () => {
|
|||||||
mouseClickHandler={handleMenuOutsideClick}
|
mouseClickHandler={handleMenuOutsideClick}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{activeTextField === 'search' && (
|
{showSearchFilter && (
|
||||||
<div className='flex gap-4'>
|
<div className='flex gap-4'>
|
||||||
<div className='grid flex-1 grid-cols-3 gap-4'>
|
<div className='grid flex-1 grid-cols-3 gap-4'>
|
||||||
<IconButtonWithMenu
|
<IconButtonWithMenu
|
||||||
@@ -597,6 +588,23 @@ const MyChores = () => {
|
|||||||
</Menu>
|
</Menu>
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
|
<IconButton
|
||||||
|
variant='outlined'
|
||||||
|
color='neutral'
|
||||||
|
size='sm'
|
||||||
|
sx={{
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 24,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setShowSearchFilter(false)
|
||||||
|
setSearchTerm('')
|
||||||
|
setFilteredChores(chores)
|
||||||
|
setSearchFilter('All')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CancelRounded />
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{searchFilter !== 'All' && (
|
{searchFilter !== 'All' && (
|
||||||
@@ -811,7 +819,7 @@ const MyChores = () => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
'z-index': 1000,
|
'z-index': 100,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -821,6 +829,7 @@ const MyChores = () => {
|
|||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 50,
|
height: 50,
|
||||||
|
zIndex: 101,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Navigate(`/chores/create`)
|
Navigate(`/chores/create`)
|
||||||
@@ -828,6 +837,27 @@ const MyChores = () => {
|
|||||||
>
|
>
|
||||||
<Add />
|
<Add />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
color='primary'
|
||||||
|
variant='soft'
|
||||||
|
sx={{
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
position: 'relative',
|
||||||
|
left: -25,
|
||||||
|
top: 22,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setAddTaskModalOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Bolt
|
||||||
|
style={{
|
||||||
|
rotate: '20deg',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={isSnackbarOpen}
|
open={isSnackbarOpen}
|
||||||
@@ -843,6 +873,19 @@ const MyChores = () => {
|
|||||||
<Typography level='title-md'>{snackBarMessage}</Typography>
|
<Typography level='title-md'>{snackBarMessage}</Typography>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
<NotificationAccessSnackbar />
|
<NotificationAccessSnackbar />
|
||||||
|
{addTaskModalOpen && (
|
||||||
|
<TaskInput
|
||||||
|
autoFocus={taskInputFocus}
|
||||||
|
onChoreUpdate={updateChores}
|
||||||
|
isModalOpen={addTaskModalOpen}
|
||||||
|
onClose={forceRefresh => {
|
||||||
|
setAddTaskModalOpen(false)
|
||||||
|
if (forceRefresh) {
|
||||||
|
refetchChores()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Sidepanel chores={chores} performers={performers} />
|
<Sidepanel chores={chores} performers={performers} />
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ import {
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { API_URL } from '../Config'
|
|
||||||
import { GetAllUsers, GetChores, MarkChoreComplete } from '../utils/Fetcher'
|
import { GetAllUsers, GetChores, MarkChoreComplete } from '../utils/Fetcher'
|
||||||
import { Fetch } from '../utils/TokenManager'
|
|
||||||
import DateModal from './Modals/Inputs/DateModal'
|
import DateModal from './Modals/Inputs/DateModal'
|
||||||
// import moment from 'moment'
|
// import moment from 'moment'
|
||||||
|
|
||||||
@@ -263,21 +261,22 @@ const ChoresOverview = () => {
|
|||||||
size='sm'
|
size='sm'
|
||||||
// sx={{ borderRadius: '50%' }}
|
// sx={{ borderRadius: '50%' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
MarkChoreComplete(chore.id,null,null,null)
|
MarkChoreComplete(chore.id, null, null, null).then(
|
||||||
.then(response => {
|
response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
response.json().then(data => {
|
response.json().then(data => {
|
||||||
const newChore = data.res
|
const newChore = data.res
|
||||||
const newChores = [...chores]
|
const newChores = [...chores]
|
||||||
const index = newChores.findIndex(
|
const index = newChores.findIndex(
|
||||||
c => c.id === chore.id,
|
c => c.id === chore.id,
|
||||||
)
|
)
|
||||||
newChores[index] = newChore
|
newChores[index] = newChore
|
||||||
setChores(newChores)
|
setChores(newChores)
|
||||||
setFilteredChores(newChores)
|
setFilteredChores(newChores)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
aria-setsize={2}
|
aria-setsize={2}
|
||||||
>
|
>
|
||||||
@@ -325,19 +324,20 @@ const ChoresOverview = () => {
|
|||||||
alert('Please select a performer')
|
alert('Please select a performer')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
MarkChoreComplete(choreId, null, date, activeUserId)
|
MarkChoreComplete(choreId, null, date, activeUserId).then(
|
||||||
.then(response => {
|
response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
response.json().then(data => {
|
response.json().then(data => {
|
||||||
const newChore = data.res
|
const newChore = data.res
|
||||||
const newChores = [...chores]
|
const newChores = [...chores]
|
||||||
const index = newChores.findIndex(c => c.id === chore.id)
|
const index = newChores.findIndex(c => c.id === chore.id)
|
||||||
newChores[index] = newChore
|
newChores[index] = newChore
|
||||||
setChores(newChores)
|
setChores(newChores)
|
||||||
setFilteredChores(newChores)
|
setFilteredChores(newChores)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import { useQuery } from 'react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { GetLabels } from '../../utils/Fetcher'
|
import { CreateLabel, GetLabels } from '../../utils/Fetcher'
|
||||||
|
|
||||||
export const useLabels = () => {
|
export const useLabels = () => {
|
||||||
return useQuery('labels', GetLabels, {
|
return useQuery({
|
||||||
initialData: [],
|
queryKey: ['labels'],
|
||||||
|
queryFn: GetLabels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreateLabel = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['createLabel'],
|
||||||
|
queryFn: CreateLabel,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,26 +11,25 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { 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 { useMutation, useQueryClient } from '@tanstack/react-query'
|
// import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Add } from '@mui/icons-material'
|
import { Add } from '@mui/icons-material'
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors'
|
import { getTextColorFromBackgroundColor } from '../../utils/Colors'
|
||||||
import { DeleteLabel } from '../../utils/Fetcher'
|
import { DeleteLabel } from '../../utils/Fetcher'
|
||||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||||
|
import { useLabels } from './LabelQueries'
|
||||||
|
|
||||||
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([])
|
||||||
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({})
|
||||||
const Navigate = useNavigate()
|
|
||||||
const handleAddLabel = () => {
|
const handleAddLabel = () => {
|
||||||
setCurrentLabel(null)
|
setCurrentLabel(null)
|
||||||
setModalOpen(true)
|
setModalOpen(true)
|
||||||
@@ -53,7 +52,7 @@ const LabelView = () => {
|
|||||||
color: 'danger',
|
color: 'danger',
|
||||||
cancelText: 'Cancel',
|
cancelText: 'Cancel',
|
||||||
onClose: confirmed => {
|
onClose: confirmed => {
|
||||||
if (confirmed) {
|
if (confirmed === true) {
|
||||||
handleDeleteLabel(id)
|
handleDeleteLabel(id)
|
||||||
}
|
}
|
||||||
setConfirmationModel({})
|
setConfirmationModel({})
|
||||||
@@ -140,7 +139,7 @@ const LabelView = () => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
onClick={() => handleDeleteLabel(label.id)}
|
onClick={() => handleDeleteClicked(label.id)}
|
||||||
color='danger'
|
color='danger'
|
||||||
>
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { useMutation, useQueryClient } from 'react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useError } from '../../../service/ErrorProvider.jsx'
|
||||||
import LABEL_COLORS from '../../../utils/Colors.jsx'
|
import LABEL_COLORS from '../../../utils/Colors.jsx'
|
||||||
import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher'
|
import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher'
|
||||||
import { useLabels } from '../../Labels/LabelQueries'
|
import { useLabels } from '../../Labels/LabelQueries'
|
||||||
@@ -22,6 +23,7 @@ function LabelModal({ isOpen, onClose, label }) {
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const { data: userLabels = [] } = useLabels()
|
const { data: userLabels = [] } = useLabels()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const { showError } = useError()
|
||||||
|
|
||||||
// Populate the form fields when editing
|
// Populate the form fields when editing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,26 +59,54 @@ function LabelModal({ isOpen, onClose, label }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mutation for saving labels
|
// Mutation for saving labels
|
||||||
const saveLabelMutation = useMutation(
|
// const saveLabelMutation = useMutation(
|
||||||
newLabel =>
|
// newLabel =>
|
||||||
label
|
// label
|
||||||
? UpdateLabel({ id: label.id, ...newLabel })
|
// ? UpdateLabel({ id: label.id, ...newLabel })
|
||||||
: CreateLabel(newLabel),
|
// : CreateLabel(newLabel),
|
||||||
{
|
// {
|
||||||
onSuccess: () => {
|
// onSuccess: () => {
|
||||||
queryClient.invalidateQueries('labels')
|
// queryClient.invalidateQueries('labels')
|
||||||
onClose()
|
// onClose()
|
||||||
},
|
// },
|
||||||
onError: () => {
|
// onError: () => {
|
||||||
setError('Failed to save label. Please try again.')
|
// setError('Failed to save label. Please try again.')
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
)
|
// )
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!validateLabel()) return
|
if (!validateLabel()) return
|
||||||
|
const saveLabel = label?.id && label.id !== -1 ? UpdateLabel : CreateLabel
|
||||||
saveLabelMutation.mutate({ name: labelName, color })
|
// ? { id: label.id, name: labelName, color }
|
||||||
|
// : { name: labelName, color }
|
||||||
|
// saveLabelMutation.mutate({ name: labelName, color })
|
||||||
|
saveLabel({
|
||||||
|
id: label?.id,
|
||||||
|
name: labelName,
|
||||||
|
color,
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res?.error) {
|
||||||
|
setError(res.error)
|
||||||
|
} else {
|
||||||
|
queryClient.invalidateQueries('labels')
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.queued) {
|
||||||
|
showError({
|
||||||
|
title: 'Failed to save label',
|
||||||
|
message: 'Unable to save label. Please try again.',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showError({
|
||||||
|
title: 'Failed to save label',
|
||||||
|
message: 'Unable to save label. Please try again.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -109,7 +139,7 @@ function LabelModal({ isOpen, onClose, label }) {
|
|||||||
<Typography
|
<Typography
|
||||||
startDecorator={
|
startDecorator={
|
||||||
<Box
|
<Box
|
||||||
className='h-4 w-4'
|
className='size-4'
|
||||||
borderRadius={10}
|
borderRadius={10}
|
||||||
sx={{ background: selected.value }}
|
sx={{ background: selected.value }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useContext, useEffect, useState } from 'react'
|
import { useContext, useEffect, useState } from 'react'
|
||||||
|
import { Navigate } from 'react-router-dom'
|
||||||
import { UserContext } from '../../contexts/UserContext'
|
import { UserContext } from '../../contexts/UserContext'
|
||||||
import Logo from '../../Logo'
|
import Logo from '../../Logo'
|
||||||
import {
|
import {
|
||||||
@@ -498,6 +499,46 @@ const Settings = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-4 py-4'>
|
||||||
|
<Typography level='h3'>Experimental Features </Typography>
|
||||||
|
<Divider />
|
||||||
|
<Typography level='body-md'>
|
||||||
|
Clean up some part of the local storage and cache. Only use if you
|
||||||
|
know are you doing.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant='soft'
|
||||||
|
color='danger'
|
||||||
|
onClick={() => {
|
||||||
|
const confirmed = confirm(
|
||||||
|
`Are you sure you want to clear your local storage and cache? This will remove all your data. on device and require login`,
|
||||||
|
)
|
||||||
|
if (confirmed) {
|
||||||
|
localStorage.clear()
|
||||||
|
Navigate('/login')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear Local Storage and Cache
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
color='danger'
|
||||||
|
onClick={() => {
|
||||||
|
const confirmed = confirm(
|
||||||
|
`Are you sure you want to clear your local storage and cache? This will remove all your data.`,
|
||||||
|
)
|
||||||
|
if (confirmed) {
|
||||||
|
localStorage.removeItem('offline_cache')
|
||||||
|
localStorage.removeItem('offline_request_queue')
|
||||||
|
localStorage.removeItem('offlineTasks')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear Offline Cache and Offline tasks
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
88
src/views/TestView/AutocompleteDropdown.jsx
Normal file
88
src/views/TestView/AutocompleteDropdown.jsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// AutocompleteDropdown.jsx
|
||||||
|
import { Divider, Menu, MenuItem } from '@mui/joy'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
|
const AutocompleteDropdown = ({
|
||||||
|
currentValue,
|
||||||
|
suggestions,
|
||||||
|
selectedIndex,
|
||||||
|
onSelectSuggestion,
|
||||||
|
onMouseEnterSuggestion, // Added for hover selection
|
||||||
|
parentRefer, // Ref to the dropdown element
|
||||||
|
}) => {
|
||||||
|
// Scroll selected item into view
|
||||||
|
const dropdownMenuRef = React.useRef(null)
|
||||||
|
useEffect(() => {
|
||||||
|
const selectedElement = dropdownMenuRef.current?.querySelector('.selected')
|
||||||
|
if (selectedElement) {
|
||||||
|
selectedElement.scrollIntoView({
|
||||||
|
block: 'nearest',
|
||||||
|
inline: 'nearest',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [selectedIndex, dropdownMenuRef])
|
||||||
|
|
||||||
|
if (!suggestions || suggestions.options === 0) {
|
||||||
|
return null // Don't render if no suggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
open={Boolean(suggestions?.options?.length)}
|
||||||
|
ref={dropdownMenuRef}
|
||||||
|
anchorEl={parentRefer.current}
|
||||||
|
onClose={() => {}}
|
||||||
|
sx={{
|
||||||
|
width: '300px',
|
||||||
|
maxHeight: '160px',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
zIndex: 1300,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{suggestions?.options
|
||||||
|
.filter(option => {
|
||||||
|
if (typeof option === 'string') {
|
||||||
|
return option.toLowerCase().includes(currentValue.toLowerCase())
|
||||||
|
}
|
||||||
|
return option[suggestions.display]
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(currentValue.toLowerCase())
|
||||||
|
})
|
||||||
|
.map((option, index) => (
|
||||||
|
<MenuItem
|
||||||
|
key={suggestions.display ? option[suggestions.value] : option}
|
||||||
|
selected={selectedIndex === index}
|
||||||
|
onClick={() => onSelectSuggestion(option)}
|
||||||
|
onMouseEnter={() => onMouseEnterSuggestion(index)} // Update selected index on hover
|
||||||
|
className={selectedIndex === index ? 'selected' : ''} // Add class for selected item
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: selectedIndex === index ? 'gray.800' : 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{suggestions.display ? option[suggestions.display] : option}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
<Divider orientation='horizontal' />
|
||||||
|
{/*
|
||||||
|
<MenuItem
|
||||||
|
selected={selectedIndex === suggestions?.options?.length}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectSuggestion(currentValue)
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: 'gray.800',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Add />
|
||||||
|
Add new Label
|
||||||
|
</MenuItem> */}
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutocompleteDropdown
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
.task-editor-container {
|
|
||||||
background-color: #2d2d2d;
|
|
||||||
color: #ffffff;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
max-width: 500px;
|
|
||||||
margin: 20px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.smart-task-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 1.2em;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-description-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 1em;
|
|
||||||
min-height: 80px;
|
|
||||||
resize: vertical;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-attributes {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attribute-tag {
|
|
||||||
background-color: #444;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 15px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-tag {
|
|
||||||
margin-left: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-select select {
|
|
||||||
padding: 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #444;
|
|
||||||
color: #ffffff;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons button {
|
|
||||||
padding: 8px 15px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1em;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-button {
|
|
||||||
background-color: #666;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-task-button {
|
|
||||||
background-color: #d9534f;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.smart-task-input {
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
/* color: transparent;
|
|
||||||
caret-color: white;
|
|
||||||
|
|
||||||
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.smart-task-display {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
/* top: 0;
|
|
||||||
left: 0; */
|
|
||||||
|
|
||||||
z-index: 10;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
overflow-y: auto;
|
|
||||||
word-wrap: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.smart-task-common {
|
|
||||||
font-size: 1.2em;
|
|
||||||
line-height: 1.2em;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight-date {
|
|
||||||
color: #f08080;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight-label {
|
|
||||||
color: #add8e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight-priority {
|
|
||||||
color: #ffb6c1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-input {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
background-color: #2d2d2d;
|
|
||||||
color: #ffffff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import './MentionTextfield.css' // We'll assume a CSS file for basic styling
|
|
||||||
|
|
||||||
// Helper function to render text with highlighting
|
|
||||||
|
|
||||||
const MentionTextfield = ({ initialTask = {}, onCancel, onAddTask }) => {
|
|
||||||
// State to hold the task details
|
|
||||||
const [taskTitle, setTaskTitle] = useState(initialTask.title || '')
|
|
||||||
const [taskDescription, setTaskDescription] = useState(
|
|
||||||
initialTask.description || '',
|
|
||||||
)
|
|
||||||
const [selectedDate, setSelectedDate] = useState(initialTask.date || null)
|
|
||||||
const [selectedPriority, setSelectedPriority] = useState(
|
|
||||||
initialTask.priority || null,
|
|
||||||
)
|
|
||||||
const [selectedLabels, setSelectedLabels] = useState(initialTask.labels || [])
|
|
||||||
const [reminders, setReminders] = useState(initialTask.reminders || [])
|
|
||||||
const [selectedProject, setSelectedProject] = useState(
|
|
||||||
initialTask.project || 'Inbox',
|
|
||||||
)
|
|
||||||
|
|
||||||
const [cursorPosition, setCursorPosition] = useState(0) // State to track cursor position
|
|
||||||
const [showCursor, setShowCursor] = useState(false) // State to toggle cursor visibility
|
|
||||||
|
|
||||||
// Ref for the input field (if needed for more advanced handling)
|
|
||||||
const titleInputRef = useRef(null)
|
|
||||||
|
|
||||||
// Effect to update bottom tags based on title parsing
|
|
||||||
useEffect(() => {
|
|
||||||
// Basic parsing logic
|
|
||||||
const lowerCaseTitle = taskTitle.toLowerCase()
|
|
||||||
const extractedDate = lowerCaseTitle.includes('tomorrow')
|
|
||||||
? 'Tomorrow'
|
|
||||||
: null
|
|
||||||
|
|
||||||
const labelMatches = taskTitle.match(/@(\w+)/g)
|
|
||||||
const extractedLabels = labelMatches
|
|
||||||
? labelMatches.map(match => match.substring(1))
|
|
||||||
: []
|
|
||||||
|
|
||||||
const priorityMatch = taskTitle.match(/P(\d)/)
|
|
||||||
const extractedPriority = priorityMatch ? `P${priorityMatch[1]}` : null
|
|
||||||
|
|
||||||
setSelectedDate(extractedDate)
|
|
||||||
setSelectedLabels(extractedLabels)
|
|
||||||
setSelectedPriority(extractedPriority)
|
|
||||||
}, [taskTitle]) // Re-run effect when taskTitle changes
|
|
||||||
|
|
||||||
// Handlers for removing attributes (same as before)
|
|
||||||
const removeDate = () => setSelectedDate(null) // Note: This won't remove from the input field text
|
|
||||||
const removePriority = () => setSelectedPriority(null) // Note: This won't remove from the input field text
|
|
||||||
const removeLabel = labelToRemove => {
|
|
||||||
// Note: This won't remove from the input field text
|
|
||||||
setSelectedLabels(selectedLabels.filter(label => label !== labelToRemove))
|
|
||||||
}
|
|
||||||
const removeReminder = reminderToRemove => {
|
|
||||||
setReminders(reminders.filter(reminder => reminder !== reminderToRemove))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddTask = () => {
|
|
||||||
const newTask = {
|
|
||||||
title: taskTitle,
|
|
||||||
description: taskDescription,
|
|
||||||
date: selectedDate, // These come from the parsing effect
|
|
||||||
priority: selectedPriority, // These come from the parsing effect
|
|
||||||
labels: selectedLabels, // These come from the parsing effect
|
|
||||||
reminders: reminders,
|
|
||||||
project: selectedProject,
|
|
||||||
}
|
|
||||||
onAddTask(newTask) // Call the provided add task function
|
|
||||||
}
|
|
||||||
const renderHighlightedText = (text, position) => {
|
|
||||||
const parts = []
|
|
||||||
let lastIndex = 0
|
|
||||||
|
|
||||||
// Regex to find patterns: Tomorrow, @label, P followed by a digit
|
|
||||||
const regex = /(Tomorrow)|(@\w+)|(P\d)/gi
|
|
||||||
let match
|
|
||||||
|
|
||||||
while ((match = regex.exec(text)) !== null) {
|
|
||||||
const matchedText = match[0]
|
|
||||||
const matchIndex = match.index
|
|
||||||
|
|
||||||
// Add text before the match
|
|
||||||
if (matchIndex > lastIndex) {
|
|
||||||
parts.push(text.substring(lastIndex, matchIndex))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the highlighted part
|
|
||||||
let className = ''
|
|
||||||
if (matchedText.toLowerCase() === 'tomorrow') {
|
|
||||||
className = 'highlight-date'
|
|
||||||
} else if (matchedText.startsWith('@')) {
|
|
||||||
className = 'highlight-label'
|
|
||||||
} else if (matchedText.startsWith('P')) {
|
|
||||||
className = 'highlight-priority'
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.push(
|
|
||||||
<span key={matchIndex} className={className}>
|
|
||||||
{matchedText}
|
|
||||||
</span>,
|
|
||||||
)
|
|
||||||
|
|
||||||
lastIndex = regex.lastIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any remaining text after the last match
|
|
||||||
if (lastIndex < text.length) {
|
|
||||||
parts.push(text.substring(lastIndex))
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>{parts}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='task-editor-container'>
|
|
||||||
<div className='task-input h-20 overflow-auto rounded border'>
|
|
||||||
<textarea
|
|
||||||
type='text'
|
|
||||||
className='smart-task-input smart-task-common'
|
|
||||||
rows={100}
|
|
||||||
placeholder='Tomorrow do some task with @test_label and P3'
|
|
||||||
value={taskTitle}
|
|
||||||
onChange={e => setTaskTitle(e.target.value)}
|
|
||||||
// onKeyUp={e => {
|
|
||||||
// setCursorPosition(e.target.selectionStart)
|
|
||||||
// }}
|
|
||||||
// onKeyDown={e => {
|
|
||||||
// setCursorPosition(e.target.selectionStart)
|
|
||||||
// }}
|
|
||||||
onBlur={() => setShowCursor(false)}
|
|
||||||
onFocus={() => setShowCursor(true)}
|
|
||||||
ref={titleInputRef}
|
|
||||||
// style={{ opacity: 0, position: 'absolute', zIndex: -1 }}
|
|
||||||
/>
|
|
||||||
{/* Display Area with Highlighting */}
|
|
||||||
<div
|
|
||||||
className='smart-task-display smart-task-common'
|
|
||||||
onClick={e => {
|
|
||||||
const range = document.caretRangeFromPoint(e.clientX, e.clientY)
|
|
||||||
if (range) {
|
|
||||||
setCursorPosition(range.startOffset)
|
|
||||||
|
|
||||||
const offset = range.startOffset
|
|
||||||
|
|
||||||
titleInputRef.current.focus()
|
|
||||||
titleInputRef.current.setSelectionRange(offset, offset)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderHighlightedText(taskTitle, cursorPosition)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Description Area */}
|
|
||||||
<textarea
|
|
||||||
className='task-description-input'
|
|
||||||
placeholder='Description'
|
|
||||||
value={taskDescription}
|
|
||||||
onChange={e => setTaskDescription(e.target.value)}
|
|
||||||
/>
|
|
||||||
{/* Attributes/Tags Display (same as before, updated by useEffect) */}
|
|
||||||
<div className='task-attributes'>
|
|
||||||
{selectedDate && (
|
|
||||||
<div className='attribute-tag'>
|
|
||||||
🗓️ {selectedDate}{' '}
|
|
||||||
<span className='remove-tag' onClick={removeDate}>
|
|
||||||
×
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{selectedPriority && (
|
|
||||||
<div className='attribute-tag'>
|
|
||||||
🚩 {selectedPriority}{' '}
|
|
||||||
<span className='remove-tag' onClick={removePriority}>
|
|
||||||
×
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{reminders.map((reminder, index) => (
|
|
||||||
<div key={index} className='attribute-tag'>
|
|
||||||
⏰ {reminder}{' '}
|
|
||||||
<span
|
|
||||||
className='remove-tag'
|
|
||||||
onClick={() => removeReminder(reminder)}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{selectedLabels.map((label, index) => (
|
|
||||||
<div key={index} className='attribute-tag'>
|
|
||||||
🏷️ {label}{' '}
|
|
||||||
<span className='remove-tag' onClick={() => removeLabel(label)}>
|
|
||||||
×
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{/* More options button - static for this example */}
|
|
||||||
<div className='attribute-tag'>...</div>
|
|
||||||
</div>
|
|
||||||
{/* Project Selection and Action Buttons (same as before) */}
|
|
||||||
<div className='task-actions'>
|
|
||||||
<div className='project-select'>
|
|
||||||
{/* In a real app, this would be a more robust dropdown */}
|
|
||||||
<select
|
|
||||||
value={selectedProject}
|
|
||||||
onChange={e => setSelectedProject(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value='Inbox'>📥 Inbox</option>
|
|
||||||
{/* Add other project options here */}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className='action-buttons'>
|
|
||||||
<button className='cancel-button' onClick={onCancel}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button className='add-task-button' onClick={handleAddTask}>
|
|
||||||
Add task
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MentionTextfield
|
|
||||||
42
src/views/TestView/SmartTaskTitleInput.css
Normal file
42
src/views/TestView/SmartTaskTitleInput.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.smart-task-display {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.smart-task-common {
|
||||||
|
font-size: 1.2em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
font-family: inherit;
|
||||||
|
caret-color: #f08080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-date {
|
||||||
|
color: #f08080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-repeat {
|
||||||
|
color: #90ee90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-label {
|
||||||
|
color: #add8e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-priority {
|
||||||
|
color: #ffb6c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-input {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
273
src/views/TestView/SmartTaskTitleInput.jsx
Normal file
273
src/views/TestView/SmartTaskTitleInput.jsx
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
import { useColorScheme } from '@mui/joy'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import AutocompleteDropdown from './AutocompleteDropdown'
|
||||||
|
import './SmartTaskTitleInput.css'
|
||||||
|
const renderHighlightedText = (text, cursorPosition) => {
|
||||||
|
const parts = []
|
||||||
|
let lastIndex = 0
|
||||||
|
|
||||||
|
const regex = /(Tomorrow)|(#\w+)|(P\d)/gi
|
||||||
|
let match
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
const matchedText = match[0]
|
||||||
|
const matchIndex = match.index
|
||||||
|
|
||||||
|
if (matchIndex > lastIndex) {
|
||||||
|
parts.push(text.substring(lastIndex, matchIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
let className = ''
|
||||||
|
if (matchedText.toLowerCase() === 'tomorrow') {
|
||||||
|
className = 'highlight-date'
|
||||||
|
} else if (matchedText.startsWith('#')) {
|
||||||
|
className = 'highlight-label'
|
||||||
|
} else if (matchedText.startsWith('P')) {
|
||||||
|
className = 'highlight-priority'
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.push(
|
||||||
|
<span key={matchIndex} className={className}>
|
||||||
|
{matchedText}
|
||||||
|
</span>,
|
||||||
|
)
|
||||||
|
|
||||||
|
lastIndex = regex.lastIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
parts.push(text.substring(lastIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
const SmartTaskTitleInput = ({
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
autoFocus,
|
||||||
|
onChange,
|
||||||
|
suggestions,
|
||||||
|
onEnterPressed,
|
||||||
|
customRenderer,
|
||||||
|
}) => {
|
||||||
|
const { mode, setMode } = useColorScheme()
|
||||||
|
const titleInputRef = useRef(null)
|
||||||
|
const [cursorPosition, setCursorPosition] = useState(value?.length)
|
||||||
|
const dropdownRef = useRef(null)
|
||||||
|
const [lastWord, setLastWord] = useState('')
|
||||||
|
const [suggestionTrigger, setSuggestionTrigger] = useState('P')
|
||||||
|
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||||
|
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCursorPosition(prevPos => Math.min(prevPos, value.length))
|
||||||
|
|
||||||
|
if (
|
||||||
|
titleInputRef.current &&
|
||||||
|
document.activeElement === titleInputRef.current
|
||||||
|
) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
titleInputRef.current.setSelectionRange(cursorPosition, cursorPosition)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [value])
|
||||||
|
useEffect(() => {
|
||||||
|
// set focus on the input when the component is mounted:
|
||||||
|
if (titleInputRef.current) {
|
||||||
|
titleInputRef.current.focus()
|
||||||
|
titleInputRef.current.setSelectionRange(cursorPosition, cursorPosition)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
const handleSuggestionChange = text => {
|
||||||
|
// if the last word start with '@' or '#' or 'P':
|
||||||
|
const lastWord = text.split(' ').pop()
|
||||||
|
if (
|
||||||
|
lastWord.startsWith('@') ||
|
||||||
|
lastWord.startsWith('#') ||
|
||||||
|
lastWord.startsWith('P')
|
||||||
|
) {
|
||||||
|
setSuggestionTrigger(lastWord[0])
|
||||||
|
// last word without the first character:
|
||||||
|
setLastWord(lastWord.slice(1))
|
||||||
|
|
||||||
|
setShowSuggestions(true)
|
||||||
|
} else {
|
||||||
|
setShowSuggestions(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTextareaChange = e => {
|
||||||
|
handleSuggestionChange(e.target.value)
|
||||||
|
onChange(e.target.value)
|
||||||
|
setCursorPosition(e.target.selectionStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTextareaKeyDown = e => {
|
||||||
|
if (showSuggestions) {
|
||||||
|
const currentSuggestions = suggestions[suggestionTrigger].options.filter(
|
||||||
|
option => {
|
||||||
|
if (typeof option === 'string') {
|
||||||
|
return option.toLowerCase().includes(lastWord.toLowerCase())
|
||||||
|
}
|
||||||
|
return option[suggestions[suggestionTrigger].display]
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(lastWord.toLowerCase())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
const newIndex =
|
||||||
|
e.key === 'ArrowDown'
|
||||||
|
? (selectedSuggestionIndex + 1) % currentSuggestions.length
|
||||||
|
: (selectedSuggestionIndex - 1 + currentSuggestions.length) %
|
||||||
|
currentSuggestions.length
|
||||||
|
setSelectedSuggestionIndex(newIndex)
|
||||||
|
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
||||||
|
e.preventDefault()
|
||||||
|
const selectedSuggestion = currentSuggestions[selectedSuggestionIndex]
|
||||||
|
const suggestionValue = suggestions[suggestionTrigger].display
|
||||||
|
? selectedSuggestion[suggestions[suggestionTrigger].display]
|
||||||
|
: selectedSuggestion
|
||||||
|
|
||||||
|
if (suggestionValue) {
|
||||||
|
const newValue = `${value.slice(0, cursorPosition - lastWord.length)}${suggestionValue} ${value.slice(cursorPosition)}`
|
||||||
|
onChange(newValue)
|
||||||
|
titleInputRef.current.value = newValue
|
||||||
|
|
||||||
|
setShowSuggestions(false)
|
||||||
|
|
||||||
|
const newCursorPosition = cursorPosition + suggestionValue.length + 1
|
||||||
|
titleInputRef.current.setSelectionRange(
|
||||||
|
newCursorPosition,
|
||||||
|
newCursorPosition,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
setShowSuggestions(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (onEnterPressed) {
|
||||||
|
onEnterPressed(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPos = e.target.selectionStart
|
||||||
|
setCursorPosition(currentPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTextareaClick = e => {
|
||||||
|
const currentPos = e.target.selectionStart
|
||||||
|
setCursorPosition(currentPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDisplayClick = e => {
|
||||||
|
const range = document.caretRangeFromPoint(e.clientX, e.clientY)
|
||||||
|
if (range) {
|
||||||
|
const offset = range.startOffset
|
||||||
|
setCursorPosition(offset)
|
||||||
|
|
||||||
|
if (titleInputRef.current) {
|
||||||
|
titleInputRef.current.focus()
|
||||||
|
titleInputRef.current.setSelectionRange(offset, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='task-input h-20 overflow-auto rounded border'>
|
||||||
|
<textarea
|
||||||
|
ref={titleInputRef}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
rows={1}
|
||||||
|
value={value}
|
||||||
|
onChange={handleTextareaChange}
|
||||||
|
onKeyDown={handleTextareaKeyDown}
|
||||||
|
onClick={handleTextareaClick}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
// opacity: 100,
|
||||||
|
zIndex: 1,
|
||||||
|
resize: 'none',
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: '0.5rem',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
lineHeight: 'inherit',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: mode === 'dark' ? '#cbd5e1' : '#1a202c',
|
||||||
|
caretColor: mode === 'dark' ? '#fff' : '#000',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className='smart-task-display smart-task-common'
|
||||||
|
ref={dropdownRef}
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
minHeight: '100%',
|
||||||
|
padding: '0.5rem',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
lineHeight: 'inherit',
|
||||||
|
}}
|
||||||
|
onClick={handleDisplayClick}
|
||||||
|
>
|
||||||
|
{placeholder && !value && (
|
||||||
|
<span className='pointer-events-none text-gray-400'>
|
||||||
|
{placeholder}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{customRenderer
|
||||||
|
? customRenderer
|
||||||
|
: renderHighlightedText(value, cursorPosition)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showSuggestions && (
|
||||||
|
<AutocompleteDropdown
|
||||||
|
currentValue={lastWord}
|
||||||
|
suggestions={suggestions[suggestionTrigger]}
|
||||||
|
selectedIndex={selectedSuggestionIndex}
|
||||||
|
onMouseEnterSuggestion={index => {
|
||||||
|
setSelectedSuggestionIndex(index)
|
||||||
|
}}
|
||||||
|
onSelectSuggestion={suggestion => {
|
||||||
|
const suggestionValue = suggestions[suggestionTrigger].display
|
||||||
|
? suggestion[suggestions[suggestionTrigger].display]
|
||||||
|
: suggestion
|
||||||
|
const newValue = `${value.slice(0, cursorPosition)}${suggestionValue}${value.slice(cursorPosition)}`
|
||||||
|
|
||||||
|
onChange(newValue)
|
||||||
|
titleInputRef?.current?.focus()
|
||||||
|
|
||||||
|
setCursorPosition(cursorPosition + suggestion.length)
|
||||||
|
titleInputRef.current.value = newValue
|
||||||
|
titleInputRef.current.setSelectionRange(
|
||||||
|
cursorPosition + suggestionValue.length,
|
||||||
|
cursorPosition + suggestionValue.length,
|
||||||
|
)
|
||||||
|
setShowSuggestions(false)
|
||||||
|
}}
|
||||||
|
parentRefer={dropdownRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SmartTaskTitleInput
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useError } from '../../service/ErrorProvider'
|
||||||
import {
|
import {
|
||||||
CreateThing,
|
CreateThing,
|
||||||
DeleteThing,
|
DeleteThing,
|
||||||
@@ -172,6 +173,7 @@ const ThingsView = () => {
|
|||||||
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
|
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
|
||||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||||
const [snackbarColor, setSnackbarColor] = useState('success')
|
const [snackbarColor, setSnackbarColor] = useState('success')
|
||||||
|
const { showError } = useError()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// fetch things
|
// fetch things
|
||||||
@@ -187,25 +189,39 @@ const ThingsView = () => {
|
|||||||
if (thing?.id) {
|
if (thing?.id) {
|
||||||
saveFunc = SaveThing
|
saveFunc = SaveThing
|
||||||
}
|
}
|
||||||
saveFunc(thing).then(result => {
|
saveFunc(thing)
|
||||||
result.json().then(data => {
|
.then(result => {
|
||||||
if (thing?.id) {
|
result.json().then(data => {
|
||||||
const currentThings = [...things]
|
if (thing?.id) {
|
||||||
const thingIndex = currentThings.findIndex(
|
const currentThings = [...things]
|
||||||
currentThing => currentThing.id === thing.id,
|
const thingIndex = currentThings.findIndex(
|
||||||
)
|
currentThing => currentThing.id === thing.id,
|
||||||
currentThings[thingIndex] = data.res
|
)
|
||||||
setThings(currentThings)
|
currentThings[thingIndex] = data.res
|
||||||
|
setThings(currentThings)
|
||||||
|
} else {
|
||||||
|
const currentThings = [...things]
|
||||||
|
currentThings.push(data.res)
|
||||||
|
setThings(currentThings)
|
||||||
|
}
|
||||||
|
setSnackbarMessage('Thing saved successfully')
|
||||||
|
setSnackbarColor('success')
|
||||||
|
setIsSnackbarOpen(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error?.queued) {
|
||||||
|
showError({
|
||||||
|
title: 'Unable to save thing',
|
||||||
|
message: 'You are offline and the request has been queued',
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
const currentThings = [...things]
|
showError({
|
||||||
currentThings.push(data.res)
|
title: 'Unable to save thing',
|
||||||
setThings(currentThings)
|
message: 'An error occurred while saving the thing',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
|
||||||
setSnackbarMessage('Thing saved successfully')
|
|
||||||
setSnackbarColor('success')
|
|
||||||
setIsSnackbarOpen(true)
|
|
||||||
}
|
}
|
||||||
const handleEditClick = thing => {
|
const handleEditClick = thing => {
|
||||||
setIsShowEditStateModal(true)
|
setIsShowEditStateModal(true)
|
||||||
@@ -220,21 +236,37 @@ const ThingsView = () => {
|
|||||||
message: 'Are you sure you want to delete this Thing?',
|
message: 'Are you sure you want to delete this Thing?',
|
||||||
onClose: isConfirmed => {
|
onClose: isConfirmed => {
|
||||||
if (isConfirmed === true) {
|
if (isConfirmed === true) {
|
||||||
DeleteThing(thing.id).then(response => {
|
DeleteThing(thing.id)
|
||||||
if (response.ok) {
|
.then(response => {
|
||||||
const currentThings = [...things]
|
if (response.ok) {
|
||||||
const thingIndex = currentThings.findIndex(
|
const currentThings = [...things]
|
||||||
currentThing => currentThing.id === thing.id,
|
const thingIndex = currentThings.findIndex(
|
||||||
)
|
currentThing => currentThing.id === thing.id,
|
||||||
currentThings.splice(thingIndex, 1)
|
)
|
||||||
setThings(currentThings)
|
currentThings.splice(thingIndex, 1)
|
||||||
} else if (response.status === 405) {
|
setThings(currentThings)
|
||||||
setSnackbarMessage('Unable to delete thing with associated tasks')
|
} else if (response.status === 405) {
|
||||||
setSnackbarColor('danger')
|
setSnackbarMessage(
|
||||||
setIsSnackbarOpen(true)
|
'Unable to delete thing with associated tasks',
|
||||||
}
|
)
|
||||||
// if method not allwo show snackbar:
|
setSnackbarColor('danger')
|
||||||
})
|
setIsSnackbarOpen(true)
|
||||||
|
}
|
||||||
|
// if method not allwo show snackbar:
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error?.queued) {
|
||||||
|
showError({
|
||||||
|
title: 'Unable to delete thing',
|
||||||
|
message: 'You are offline and the request has been queued',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showError({
|
||||||
|
title: 'Unable to delete thing',
|
||||||
|
message: 'An error occurred while deleting the thing',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
setConfirmModelConfig({})
|
setConfirmModelConfig({})
|
||||||
},
|
},
|
||||||
@@ -252,19 +284,32 @@ const ThingsView = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateThingState(thing).then(result => {
|
UpdateThingState(thing)
|
||||||
result.json().then(data => {
|
.then(result => {
|
||||||
const currentThings = [...things]
|
result.json().then(data => {
|
||||||
const thingIndex = currentThings.findIndex(
|
const currentThings = [...things]
|
||||||
currentThing => currentThing.id === thing.id,
|
const thingIndex = currentThings.findIndex(
|
||||||
)
|
currentThing => currentThing.id === thing.id,
|
||||||
currentThings[thingIndex] = data.res
|
)
|
||||||
setThings(currentThings)
|
currentThings[thingIndex] = data.res
|
||||||
|
setThings(currentThings)
|
||||||
|
setSnackbarMessage('Thing state updated successfully')
|
||||||
|
setIsSnackbarOpen(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error?.queued) {
|
||||||
|
showError({
|
||||||
|
title: 'Unable to update thing state',
|
||||||
|
message: 'You are offline and the request has been queued',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showError({
|
||||||
|
title: 'Unable to update thing state',
|
||||||
|
message: 'An error occurred while updating the thing state',
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
setSnackbarMessage('Thing state updated successfully')
|
|
||||||
setIsSnackbarOpen(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
|||||||
import CircleIcon from '@mui/icons-material/Circle'
|
import CircleIcon from '@mui/icons-material/Circle'
|
||||||
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts'
|
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts'
|
||||||
|
|
||||||
import { EventBusy, Style, Toll } from '@mui/icons-material'
|
import { EventBusy, Toll } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
@@ -28,7 +28,6 @@ import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
|
|||||||
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
||||||
import { ChoresGrouper } from '../../utils/Chores'
|
import { ChoresGrouper } from '../../utils/Chores'
|
||||||
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
||||||
import IconButtonWithMenu from '../Chores/IconButtonWithMenu'
|
|
||||||
import LoadingComponent from '../components/Loading'
|
import LoadingComponent from '../components/Loading'
|
||||||
|
|
||||||
const groupByDate = history => {
|
const groupByDate = history => {
|
||||||
@@ -204,7 +203,7 @@ const UserActivites = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isChoresHistoryLoading && !isChoresLoading && choresHistory) {
|
if (!isChoresHistoryLoading && !isChoresLoading && choresHistory) {
|
||||||
const enrichedHistory = choresHistory.res.map(item => {
|
const enrichedHistory = choresHistory.map(item => {
|
||||||
const chore = choresData.res.find(chore => chore.id === item.choreId)
|
const chore = choresData.res.find(chore => chore.id === item.choreId)
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@@ -235,7 +234,7 @@ const UserActivites = () => {
|
|||||||
var assignedToMe = 0
|
var assignedToMe = 0
|
||||||
var assignedToOthers = 0
|
var assignedToOthers = 0
|
||||||
chores.forEach(chore => {
|
chores.forEach(chore => {
|
||||||
if (chore.assignedTo === userProfile.id) {
|
if (chore.assignedTo === userProfile?.id) {
|
||||||
assignedToMe++
|
assignedToMe++
|
||||||
} else assignedToOthers++
|
} else assignedToOthers++
|
||||||
})
|
})
|
||||||
@@ -342,7 +341,7 @@ const UserActivites = () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!choresData.res?.length > 0 || !choresHistory?.res?.length > 0) {
|
if (!choresData.res?.length > 0 || !choresHistory?.length > 0) {
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
maxWidth='md'
|
maxWidth='md'
|
||||||
@@ -386,7 +385,7 @@ const UserActivites = () => {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box mb={1}>
|
||||||
<Typography mb={2} level='h4'>
|
<Typography mb={2} level='h4'>
|
||||||
Points Overview
|
Points Overview
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -487,21 +486,6 @@ const UserActivites = () => {
|
|||||||
))}
|
))}
|
||||||
</TabList>
|
</TabList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Box sx={{ ml: 2 }}>
|
|
||||||
<IconButtonWithMenu
|
|
||||||
k={'icon-menu-labels-filter'}
|
|
||||||
// label={' Labels'}
|
|
||||||
icon={<Style />}
|
|
||||||
options={[{ name: 'Anyone' }, { name: 'Me' }]}
|
|
||||||
selectedItem={'Anyone'}
|
|
||||||
onItemSelect={selected => {
|
|
||||||
console.log(selected)
|
|
||||||
}}
|
|
||||||
// isActive={searchFilter.startsWith('Label: ')}
|
|
||||||
// mouseClickHandler={handleMenuOutsideClick}
|
|
||||||
// useChips
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ mb: 4 }}>
|
<Box sx={{ mb: 4 }}>
|
||||||
<Typography level='h4' textAlign='center'>
|
<Typography level='h4' textAlign='center'>
|
||||||
|
|||||||
@@ -65,13 +65,13 @@ const UserPoints = () => {
|
|||||||
if (choresHistoryData) {
|
if (choresHistoryData) {
|
||||||
var history
|
var history
|
||||||
if (tabValue === 7) {
|
if (tabValue === 7) {
|
||||||
history = generateWeeklySummary(choresHistoryData.res, selectedUser)
|
history = generateWeeklySummary(choresHistoryData, selectedUser)
|
||||||
} else if (tabValue === 30) {
|
} else if (tabValue === 30) {
|
||||||
history = generateMonthSummary(choresHistoryData.res, selectedUser)
|
history = generateMonthSummary(choresHistoryData, selectedUser)
|
||||||
} else if (tabValue === 6 * 30) {
|
} else if (tabValue === 6 * 30) {
|
||||||
history = generateMonthlySummary(choresHistoryData.res, selectedUser)
|
history = generateMonthlySummary(choresHistoryData, selectedUser)
|
||||||
} else {
|
} else {
|
||||||
history = generateYearlySummary(choresHistoryData.res, selectedUser)
|
history = generateYearlySummary(choresHistoryData, selectedUser)
|
||||||
}
|
}
|
||||||
setSelectedHistory(history)
|
setSelectedHistory(history)
|
||||||
}
|
}
|
||||||
@@ -394,7 +394,7 @@ const UserPoints = () => {
|
|||||||
{ label: '7 Days', value: 7 },
|
{ label: '7 Days', value: 7 },
|
||||||
// { label: '3 Month', value: 30 },
|
// { label: '3 Month', value: 30 },
|
||||||
{ label: '6 Months', value: 6 * 30 },
|
{ label: '6 Months', value: 6 * 30 },
|
||||||
{ label: 'All Time', value: 90 },
|
{ label: 'All Time', value: 24 * 30 },
|
||||||
].map((tab, index) => (
|
].map((tab, index) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { KeyboardReturnOutlined } from '@mui/icons-material'
|
import { Add } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Chip,
|
Chip,
|
||||||
IconButton,
|
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalDialog,
|
ModalDialog,
|
||||||
@@ -18,63 +17,20 @@ import * as chrono from 'chrono-node'
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useContext, useEffect, useRef, useState } from 'react'
|
import { useContext, useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { CSSTransition } from 'react-transition-group'
|
|
||||||
import { UserContext } from '../../contexts/UserContext'
|
import { UserContext } from '../../contexts/UserContext'
|
||||||
import { useCreateChore } from '../../queries/ChoreQueries'
|
import { useCreateChore } from '../../queries/ChoreQueries'
|
||||||
|
import { useAllUsers } from '../../queries/UserQueries'
|
||||||
import useDebounce from '../../utils/Debounce'
|
import useDebounce from '../../utils/Debounce'
|
||||||
import { isPlusAccount } from '../../utils/Helpers'
|
import { isPlusAccount } from '../../utils/Helpers'
|
||||||
import { useLabels } from '../Labels/LabelQueries'
|
import { useLabels } from '../Labels/LabelQueries'
|
||||||
|
import SmartTaskTitleInput from '../TestView/SmartTaskTitleInput'
|
||||||
|
import { parseLabels, parsePriority, parseRepeatV2 } from './CustomParsers'
|
||||||
import LearnMoreButton from './LearnMore'
|
import LearnMoreButton from './LearnMore'
|
||||||
import SubTasks from './SubTask'
|
import SubTasks from './SubTask'
|
||||||
const VALID_DAYS = {
|
|
||||||
monday: 'Monday',
|
|
||||||
mon: 'Monday',
|
|
||||||
tuesday: 'Tuesday',
|
|
||||||
tue: 'Tuesday',
|
|
||||||
wednesday: 'Wednesday',
|
|
||||||
wed: 'Wednesday',
|
|
||||||
thursday: 'Thursday',
|
|
||||||
thu: 'Thursday',
|
|
||||||
friday: 'Friday',
|
|
||||||
fri: 'Friday',
|
|
||||||
saturday: 'Saturday',
|
|
||||||
sat: 'Saturday',
|
|
||||||
sunday: 'Sunday',
|
|
||||||
sun: 'Sunday',
|
|
||||||
}
|
|
||||||
|
|
||||||
const VALID_MONTHS = {
|
const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||||
january: 'January',
|
|
||||||
jan: 'January',
|
|
||||||
february: 'February',
|
|
||||||
feb: 'February',
|
|
||||||
march: 'March',
|
|
||||||
mar: 'March',
|
|
||||||
april: 'April',
|
|
||||||
apr: 'April',
|
|
||||||
may: 'May',
|
|
||||||
june: 'June',
|
|
||||||
jun: 'June',
|
|
||||||
july: 'July',
|
|
||||||
jul: 'July',
|
|
||||||
august: 'August',
|
|
||||||
aug: 'August',
|
|
||||||
september: 'September',
|
|
||||||
sep: 'September',
|
|
||||||
october: 'October',
|
|
||||||
oct: 'October',
|
|
||||||
november: 'November',
|
|
||||||
nov: 'November',
|
|
||||||
december: 'December',
|
|
||||||
dec: 'December',
|
|
||||||
}
|
|
||||||
|
|
||||||
const ALL_MONTHS = Object.values(VALID_MONTHS).filter(
|
|
||||||
(v, i, a) => a.indexOf(v) === i,
|
|
||||||
)
|
|
||||||
|
|
||||||
const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
|
||||||
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
||||||
|
const { data: allUsers, isLoading: isAllUserLoading } = useAllUsers()
|
||||||
const createChoreMutation = useCreateChore()
|
const createChoreMutation = useCreateChore()
|
||||||
|
|
||||||
const { userProfile } = useContext(UserContext)
|
const { userProfile } = useContext(UserContext)
|
||||||
@@ -82,24 +38,29 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
|||||||
const [taskText, setTaskText] = useState('')
|
const [taskText, setTaskText] = useState('')
|
||||||
const debounceParsing = useDebounce(taskText, 30)
|
const debounceParsing = useDebounce(taskText, 30)
|
||||||
const [taskTitle, setTaskTitle] = useState('')
|
const [taskTitle, setTaskTitle] = useState('')
|
||||||
const [openModal, setOpenModal] = useState(false)
|
const [renderedParts, setRenderedParts] = useState([])
|
||||||
|
|
||||||
const textareaRef = useRef(null)
|
const textareaRef = useRef(null)
|
||||||
const mainInputRef = useRef(null)
|
const mainInputRef = useRef(null)
|
||||||
const [priority, setPriority] = useState(0)
|
const [priority, setPriority] = useState(0)
|
||||||
const [dueDate, setDueDate] = useState(null)
|
const [dueDate, setDueDate] = useState(null)
|
||||||
const [description, setDescription] = useState(null)
|
const [description, setDescription] = useState(null)
|
||||||
|
const [assignedTo, setAssignedTo] = useState(userProfile?.id)
|
||||||
|
const [assignees, setAssignees] = useState([])
|
||||||
|
const [labelsV2, setLabelsV2] = useState([])
|
||||||
const [frequency, setFrequency] = useState(null)
|
const [frequency, setFrequency] = useState(null)
|
||||||
const [frequencyHumanReadable, setFrequencyHumanReadable] = useState(null)
|
const [frequencyHumanReadable, setFrequencyHumanReadable] = useState(null)
|
||||||
const [subTasks, setSubTasks] = useState([])
|
const [subTasks, setSubTasks] = useState(null)
|
||||||
const [hasDescription, setHasDescription] = useState(false)
|
const [hasDescription, setHasDescription] = useState(false)
|
||||||
|
const [hasSubTasks, setHasSubTasks] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openModal && textareaRef.current) {
|
if (isModalOpen && textareaRef.current) {
|
||||||
textareaRef.current.focus()
|
textareaRef.current.focus()
|
||||||
textareaRef.current.selectionStart = textareaRef.current.value?.length
|
textareaRef.current.selectionStart = textareaRef.current.value?.length
|
||||||
textareaRef.current.selectionEnd = textareaRef.current.value?.length
|
textareaRef.current.selectionEnd = textareaRef.current.value?.length
|
||||||
}
|
}
|
||||||
}, [openModal])
|
}, [isModalOpen])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoFocus > 0 && mainInputRef.current) {
|
if (autoFocus > 0 && mainInputRef.current) {
|
||||||
@@ -110,21 +71,19 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
|||||||
}, [autoFocus])
|
}, [autoFocus])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debounceParsing) {
|
if (!isModalOpen || userLabelsLoading || isAllUserLoading) {
|
||||||
processText(debounceParsing)
|
return
|
||||||
}
|
}
|
||||||
}, [debounceParsing])
|
|
||||||
|
|
||||||
const handleEnterPressed = e => {
|
processText(taskText)
|
||||||
if (e.key === 'Enter') {
|
}, [taskText, userLabelsLoading, isAllUserLoading])
|
||||||
createChore()
|
|
||||||
handleCloseModal()
|
const handleEnterPressed = () => {
|
||||||
setTaskText('')
|
createChore()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = forceRefetch => {
|
||||||
setOpenModal(false)
|
onClose(forceRefetch)
|
||||||
setTaskText('')
|
setTaskText('')
|
||||||
setTaskTitle('')
|
setTaskTitle('')
|
||||||
setDueDate(null)
|
setDueDate(null)
|
||||||
@@ -133,7 +92,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
|||||||
setPriority(0)
|
setPriority(0)
|
||||||
setHasDescription(false)
|
setHasDescription(false)
|
||||||
setDescription(null)
|
setDescription(null)
|
||||||
setSubTasks([])
|
setSubTasks(null)
|
||||||
|
setHasSubTasks(false)
|
||||||
|
setLabelsV2([])
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
@@ -142,245 +103,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
|||||||
setTaskText('')
|
setTaskText('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsePriority = inputSentence => {
|
|
||||||
let sentence = inputSentence.toLowerCase()
|
|
||||||
const priorityMap = {
|
|
||||||
1: ['p1', 'priority 1', 'high priority', 'urgent', 'asap', 'important'],
|
|
||||||
2: ['p2', 'priority 2', 'medium priority'],
|
|
||||||
3: ['p3', 'priority 3', 'low priority'],
|
|
||||||
4: ['p4', 'priority 4'],
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [priority, terms] of Object.entries(priorityMap)) {
|
|
||||||
if (terms.some(term => sentence.includes(term))) {
|
|
||||||
return {
|
|
||||||
result: priority,
|
|
||||||
cleanedSentence: terms.reduce(
|
|
||||||
(s, t) => s.replace(t, ''),
|
|
||||||
inputSentence,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { result: 0, cleanedSentence: inputSentence }
|
|
||||||
}
|
|
||||||
const parseLabels = inputSentence => {
|
|
||||||
let sentence = inputSentence.toLowerCase()
|
|
||||||
const currentLabels = []
|
|
||||||
// label will always be prefixed #:
|
|
||||||
for (const label of userLabels) {
|
|
||||||
if (sentence.includes(`#${label.name.toLowerCase()}`)) {
|
|
||||||
currentLabels.push(label)
|
|
||||||
sentence = sentence.replace(`#${label.name.toLowerCase()}`, '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentLabels.length > 0) {
|
|
||||||
return {
|
|
||||||
result: currentLabels,
|
|
||||||
cleanedSentence: sentence,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { result: null, cleanedSentence: sentence }
|
|
||||||
}
|
|
||||||
const parseAssignee = inputSentence => {
|
|
||||||
let sentence = inputSentence.toLowerCase()
|
|
||||||
const assigneeMap = {}
|
|
||||||
}
|
|
||||||
const parseRepeatV2 = inputSentence => {
|
|
||||||
const sentence = inputSentence.toLowerCase()
|
|
||||||
const result = {
|
|
||||||
frequency: 1,
|
|
||||||
frequencyType: null,
|
|
||||||
frequencyMetadata: {
|
|
||||||
days: [],
|
|
||||||
months: [],
|
|
||||||
unit: null,
|
|
||||||
time: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const patterns = [
|
|
||||||
{
|
|
||||||
frequencyType: 'day_of_the_month:every',
|
|
||||||
regex: /(\d+)(?:th|st|nd|rd)? of every month$/i,
|
|
||||||
name: 'Every {day} of every month',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frequencyType: 'daily',
|
|
||||||
regex: /(every day|daily|everyday)$/i,
|
|
||||||
name: 'Every day',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frequencyType: 'daily:time',
|
|
||||||
regex: /every (morning|noon|afternoon|evening|night)$/i,
|
|
||||||
name: 'Every {time} daily',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frequencyType: 'weekly',
|
|
||||||
regex: /(every week|weekly)$/i,
|
|
||||||
name: 'Every week',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frequencyType: 'monthly',
|
|
||||||
regex: /(every month|monthly)$/i,
|
|
||||||
name: 'Every month',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frequencyType: 'yearly',
|
|
||||||
regex: /every year$/i,
|
|
||||||
name: 'Every year',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frequencyType: 'monthly',
|
|
||||||
regex: /every (?:other )?month$/i,
|
|
||||||
name: 'Bi Monthly',
|
|
||||||
value: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frequencyType: 'interval:2week',
|
|
||||||
regex: /(bi-?weekly|every other week)/i,
|
|
||||||
value: 2,
|
|
||||||
name: 'Bi Weekly',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frequencyType: 'interval',
|
|
||||||
regex: /every (\d+) (days?|weeks?|months?|years?).*$/i,
|
|
||||||
name: 'Every {frequency} {unit}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frequencyType: 'interval:every_other',
|
|
||||||
regex: /every other (days?|weeks?|months?|years?)$/i,
|
|
||||||
name: 'Every other {unit}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frequencyType: 'days_of_the_week',
|
|
||||||
regex: /every ([\w, ]+(?:day)?(?:, [\w, ]+(?:day)?)*)$/i,
|
|
||||||
name: 'Every {days}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frequencyType: 'day_of_the_month',
|
|
||||||
regex: /(\d+)(?:st|nd|rd|th)? of ([\w ]+(?:(?:,| and |\s)[\w ]+)*)/i,
|
|
||||||
name: 'Every {day} days of {months}',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
|
||||||
const match = sentence.match(pattern.regex)
|
|
||||||
if (!match) continue
|
|
||||||
|
|
||||||
result.frequencyType = pattern.frequencyType
|
|
||||||
const unitMap = {
|
|
||||||
daily: 'days',
|
|
||||||
weekly: 'weeks',
|
|
||||||
monthly: 'months',
|
|
||||||
yearly: 'years',
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (pattern.frequencyType) {
|
|
||||||
case 'daily':
|
|
||||||
case 'weekly':
|
|
||||||
case 'monthly':
|
|
||||||
case 'yearly':
|
|
||||||
result.frequencyType = 'interval'
|
|
||||||
result.frequency = pattern.value || 1
|
|
||||||
result.frequencyMetadata.unit = unitMap[pattern.frequencyType]
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
name: pattern.name,
|
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'interval':
|
|
||||||
result.frequency = parseInt(match[1], 10)
|
|
||||||
result.frequencyMetadata.unit = match[2]
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
name: pattern.name
|
|
||||||
.replace('{frequency}', result.frequency)
|
|
||||||
.replace('{unit}', result.frequencyMetadata.unit),
|
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'days_of_the_week':
|
|
||||||
result.frequencyMetadata.days = match[1]
|
|
||||||
.toLowerCase()
|
|
||||||
.split(/ and |,|\s/)
|
|
||||||
.map(day => day.trim())
|
|
||||||
.filter(day => VALID_DAYS[day])
|
|
||||||
.map(day => VALID_DAYS[day])
|
|
||||||
if (!result.frequencyMetadata.days.length)
|
|
||||||
return { result: null, name: null, cleanedSentence: inputSentence }
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
name: pattern.name.replace(
|
|
||||||
'{days}',
|
|
||||||
result.frequencyMetadata.days.join(', '),
|
|
||||||
),
|
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'day_of_the_month':
|
|
||||||
result.frequency = parseInt(match[1], 10)
|
|
||||||
result.frequencyMetadata.months = match[2]
|
|
||||||
.toLowerCase()
|
|
||||||
.split(/ and |,|\s/)
|
|
||||||
.map(month => month.trim())
|
|
||||||
.filter(month => VALID_MONTHS[month])
|
|
||||||
.map(month => VALID_MONTHS[month])
|
|
||||||
result.frequencyMetadata.unit = 'days'
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
name: pattern.name
|
|
||||||
.replace('{day}', result.frequency)
|
|
||||||
.replace('{months}', result.frequencyMetadata.months.join(', ')),
|
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
||||||
}
|
|
||||||
case 'interval:2week':
|
|
||||||
result.frequency = 2
|
|
||||||
result.frequencyMetadata.unit = 'weeks'
|
|
||||||
result.frequencyType = 'interval'
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
name: pattern.name,
|
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
||||||
}
|
|
||||||
case 'daily:time':
|
|
||||||
result.frequency = 1
|
|
||||||
result.frequencyMetadata.unit = 'days'
|
|
||||||
result.frequencyType = 'daily'
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
name: pattern.name.replace('{time}', match[1]),
|
|
||||||
// replace every x with ''
|
|
||||||
|
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'day_of_the_month:every':
|
|
||||||
result.frequency = parseInt(match[1], 10)
|
|
||||||
result.frequencyMetadata.months = ALL_MONTHS
|
|
||||||
result.frequencyMetadata.unit = 'days'
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
name: pattern.name
|
|
||||||
.replace('{day}', result.frequency)
|
|
||||||
.replace('{months}', result.frequencyMetadata.months.join(', ')),
|
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
||||||
}
|
|
||||||
case 'interval:every_other':
|
|
||||||
result.frequency = 2
|
|
||||||
result.frequencyMetadata.unit = match[1]
|
|
||||||
result.frequencyType = 'interval'
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
name: pattern.name.replace('{unit}', result.frequencyMetadata.unit),
|
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { result: null, name: null, cleanedSentence: inputSentence }
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTextChange = e => {
|
const handleTextChange = e => {
|
||||||
if (!e.target.value) {
|
if (!e.target.value) {
|
||||||
setTaskText('')
|
setTaskText('')
|
||||||
@@ -394,18 +116,27 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
|||||||
}
|
}
|
||||||
const processText = sentence => {
|
const processText = sentence => {
|
||||||
let cleanedSentence = sentence
|
let cleanedSentence = sentence
|
||||||
const priority = parsePriority(cleanedSentence)
|
const priority = parsePriority(sentence)
|
||||||
if (priority.result) setPriority(priority.result)
|
if (priority.result) setPriority(priority.result)
|
||||||
cleanedSentence = priority.cleanedSentence
|
cleanedSentence = priority.cleanedSentence
|
||||||
|
const labels = parseLabels(sentence, userLabels)
|
||||||
|
if (labels.result) {
|
||||||
|
cleanedSentence = labels.cleanedSentence
|
||||||
|
setLabelsV2(labels.result)
|
||||||
|
}
|
||||||
|
|
||||||
const repeat = parseRepeatV2(cleanedSentence)
|
const repeat = parseRepeatV2(sentence)
|
||||||
if (repeat.result) {
|
if (repeat.result) {
|
||||||
setFrequency(repeat.result)
|
setFrequency(repeat.result)
|
||||||
setFrequencyHumanReadable(repeat.name)
|
setFrequencyHumanReadable(repeat.name)
|
||||||
cleanedSentence = repeat.cleanedSentence
|
cleanedSentence = repeat.cleanedSentence
|
||||||
}
|
}
|
||||||
|
// const assignees = parseAssignees(sentence)
|
||||||
const parsedDueDate = chrono.parse(cleanedSentence, new Date(), {
|
// if (assignees.result) {
|
||||||
|
// cleanedSentence = assignees.cleanedSentence
|
||||||
|
// set
|
||||||
|
// }
|
||||||
|
const parsedDueDate = chrono.parse(sentence, new Date(), {
|
||||||
forwardDate: true,
|
forwardDate: true,
|
||||||
})
|
})
|
||||||
if (parsedDueDate[0]?.index > -1) {
|
if (parsedDueDate[0]?.index > -1) {
|
||||||
@@ -428,14 +159,106 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (priority.result || parsedDueDate[0]?.index > -1 || repeat.result) {
|
|
||||||
setOpenModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
setTaskText(sentence)
|
setTaskText(sentence)
|
||||||
setTaskTitle(cleanedSentence.trim())
|
setTaskTitle(cleanedSentence.trim())
|
||||||
}
|
const rendered = renderText(
|
||||||
|
sentence,
|
||||||
|
repeat.highlight,
|
||||||
|
priority.highlight,
|
||||||
|
labels.highlight,
|
||||||
|
parsedDueDate && parsedDueDate[0]
|
||||||
|
? {
|
||||||
|
start: parsedDueDate[0].index,
|
||||||
|
end: parsedDueDate[0].index + parsedDueDate[0].text.length,
|
||||||
|
text: parsedDueDate[0].text,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
|
||||||
|
setRenderedParts(rendered)
|
||||||
|
}
|
||||||
|
const renderText = (
|
||||||
|
sentence,
|
||||||
|
repeatHighlight,
|
||||||
|
priorityHighlight,
|
||||||
|
labelsHighlight,
|
||||||
|
dueDateHighlight,
|
||||||
|
) => {
|
||||||
|
const parts = []
|
||||||
|
let lastIndex = 0
|
||||||
|
|
||||||
|
// Combine all highlight ranges and sort them by their start index
|
||||||
|
const allHighlights = []
|
||||||
|
if (repeatHighlight) {
|
||||||
|
repeatHighlight.forEach(h => allHighlights.push({ ...h, type: 'repeat' }))
|
||||||
|
}
|
||||||
|
if (priorityHighlight) {
|
||||||
|
priorityHighlight.forEach(h =>
|
||||||
|
allHighlights.push({ ...h, type: 'priority' }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (labelsHighlight) {
|
||||||
|
labelsHighlight.forEach(h => allHighlights.push({ ...h, type: 'label' }))
|
||||||
|
}
|
||||||
|
if (dueDateHighlight) {
|
||||||
|
allHighlights.push({ ...dueDateHighlight, type: 'dueDate' })
|
||||||
|
}
|
||||||
|
|
||||||
|
allHighlights.sort((a, b) => a.start - b.start)
|
||||||
|
|
||||||
|
for (const highlight of allHighlights) {
|
||||||
|
// Add the text before the highlight
|
||||||
|
if (highlight.start > lastIndex) {
|
||||||
|
parts.push(sentence.substring(lastIndex, highlight.start))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the class name based on the highlight type
|
||||||
|
let className = ''
|
||||||
|
switch (highlight.type) {
|
||||||
|
case 'repeat':
|
||||||
|
className = 'highlight-repeat'
|
||||||
|
break
|
||||||
|
case 'priority':
|
||||||
|
className = 'highlight-priority'
|
||||||
|
break
|
||||||
|
case 'label':
|
||||||
|
className = 'highlight-label'
|
||||||
|
break
|
||||||
|
case 'dueDate':
|
||||||
|
className = 'highlight-date'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the highlighted span
|
||||||
|
parts.push(
|
||||||
|
<span
|
||||||
|
key={highlight.start}
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
// text underline:
|
||||||
|
textDecoration: 'underline',
|
||||||
|
// textDecorationColor: 'red',
|
||||||
|
textDecorationThickness: '2px',
|
||||||
|
textDecorationStyle: 'dashed',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sentence.substring(highlight.start, highlight.end)}
|
||||||
|
</span>,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update the last index to the end of the current highlight
|
||||||
|
lastIndex = highlight.end
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining text after the last highlight
|
||||||
|
if (lastIndex < sentence.length) {
|
||||||
|
parts.push(sentence.substring(lastIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
const createChore = () => {
|
const createChore = () => {
|
||||||
const chore = {
|
const chore = {
|
||||||
name: taskTitle,
|
name: taskTitle,
|
||||||
@@ -446,13 +269,13 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
|||||||
isRolling: false,
|
isRolling: false,
|
||||||
notification: false,
|
notification: false,
|
||||||
description: description || null,
|
description: description || null,
|
||||||
labelsV2: [],
|
labelsV2: labelsV2,
|
||||||
priority: priority ? Number(priority) : 0,
|
priority: priority ? Number(priority) : 0,
|
||||||
status: 0,
|
status: 0,
|
||||||
frequencyType: 'once',
|
frequencyType: 'once',
|
||||||
frequencyMetadata: {},
|
frequencyMetadata: {},
|
||||||
notificationMetadata: {},
|
notificationMetadata: {},
|
||||||
subTasks: subTasks.length > 0 ? subTasks : null,
|
subTasks: subTasks?.length > 0 ? subTasks : null,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frequency) {
|
if (frequency) {
|
||||||
@@ -469,78 +292,51 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
|||||||
chore.nextDueDate = new Date(dueDate).toUTCString()
|
chore.nextDueDate = new Date(dueDate).toUTCString()
|
||||||
}
|
}
|
||||||
|
|
||||||
createChoreMutation.mutateAsync(chore).then(resp => {
|
createChoreMutation
|
||||||
resp.json().then(data => {
|
.mutateAsync(chore)
|
||||||
if (resp.status !== 200) {
|
.then(resp => {
|
||||||
console.error('Error creating chore:', data)
|
resp.json().then(data => {
|
||||||
return
|
if (resp.status !== 200) {
|
||||||
} else {
|
console.error('Error creating chore:', data)
|
||||||
onChoreUpdate({ ...chore, id: data.res, nextDueDate: chore.dueDate })
|
return
|
||||||
|
} else {
|
||||||
|
onChoreUpdate({
|
||||||
|
...chore,
|
||||||
|
id: data.res,
|
||||||
|
nextDueDate: chore.dueDate,
|
||||||
|
})
|
||||||
|
|
||||||
|
handleCloseModal(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error?.queued) {
|
||||||
|
handleCloseModal(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
if (userLabelsLoading || isAllUserLoading) {
|
||||||
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Modal open={isModalOpen} onClose={handleCloseModal}>
|
||||||
{!openModal && (
|
<ModalOverflow>
|
||||||
<CSSTransition in={!openModal} timeout={300} classNames='fade'>
|
<ModalDialog size='lg' sx={{ minWidth: '80%' }}>
|
||||||
<Input
|
<Typography level='h4'>Create new task</Typography>
|
||||||
autoFocus={autoFocus > 0}
|
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
|
||||||
ref={mainInputRef}
|
Experimental Feature
|
||||||
placeholder='Add a task...'
|
</Chip>
|
||||||
value={taskText}
|
<Box>
|
||||||
onChange={handleTextChange}
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: '16px',
|
display: 'flex',
|
||||||
mt: 1,
|
flexDirection: 'row',
|
||||||
mb: 1,
|
alignItems: 'center',
|
||||||
borderRadius: 24,
|
}}
|
||||||
height: 24,
|
>
|
||||||
borderColor: 'text.disabled',
|
|
||||||
padding: 1,
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
onKeyUp={handleEnterPressed}
|
|
||||||
endDecorator={
|
|
||||||
<IconButton
|
|
||||||
variant='outlined'
|
|
||||||
sx={{ borderRadius: 24, marginRight: -0.5 }}
|
|
||||||
>
|
|
||||||
<KeyboardReturnOutlined />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</CSSTransition>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Modal open={openModal} onClose={handleCloseModal}>
|
|
||||||
<ModalOverflow>
|
|
||||||
<ModalDialog>
|
|
||||||
{/* <Button
|
|
||||||
size='sm'
|
|
||||||
onClick={() => navigate(`/chores/create`)}
|
|
||||||
variant='outlined'
|
|
||||||
sx={{ position: 'absolute', right: 20 }}
|
|
||||||
startDecorator={<OpenInFull />}
|
|
||||||
>
|
|
||||||
Full Editor
|
|
||||||
</Button> */}
|
|
||||||
<Typography level='h4'>Create new task</Typography>
|
|
||||||
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
|
|
||||||
Experimental Feature
|
|
||||||
</Chip>
|
|
||||||
<Box>
|
|
||||||
<Typography level='body-sm'>Task in a sentence:</Typography>
|
<Typography level='body-sm'>Task in a sentence:</Typography>
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
ref={textareaRef}
|
|
||||||
value={taskText}
|
|
||||||
onChange={handleTextChange}
|
|
||||||
onKeyUp={handleEnterPressed}
|
|
||||||
placeholder='Type your full text here...'
|
|
||||||
sx={{ width: '100%', fontSize: '16px' }}
|
|
||||||
/>
|
|
||||||
<LearnMoreButton
|
<LearnMoreButton
|
||||||
content={
|
content={
|
||||||
<>
|
<>
|
||||||
@@ -585,58 +381,125 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
|
||||||
|
<SmartTaskTitleInput
|
||||||
|
autoFocus
|
||||||
|
value={taskText}
|
||||||
|
placeholder='Type your full text here...'
|
||||||
|
onChange={text => {
|
||||||
|
setTaskText(text)
|
||||||
|
}}
|
||||||
|
customRenderer={renderedParts}
|
||||||
|
onEnterPressed={handleEnterPressed}
|
||||||
|
suggestions={{
|
||||||
|
'#': {
|
||||||
|
value: 'id',
|
||||||
|
display: 'name',
|
||||||
|
options: userLabels ? userLabels : [],
|
||||||
|
},
|
||||||
|
'!': ['P1', 'P2', 'P3', 'P4'],
|
||||||
|
'@': {
|
||||||
|
// value: 'userId',
|
||||||
|
// display: 'displayName',
|
||||||
|
// options: allUsers ? allUsers : [],
|
||||||
|
value: 'id',
|
||||||
|
display: 'name',
|
||||||
|
options: [
|
||||||
|
{ id: userProfile.id, name: userProfile.displayName },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{/* <Box>
|
||||||
<Typography level='body-sm'>Title:</Typography>
|
<Typography level='body-sm'>Title:</Typography>
|
||||||
<Input
|
<Input
|
||||||
value={taskTitle}
|
value={taskTitle}
|
||||||
onChange={e => setTaskTitle(e.target.value)}
|
onChange={e => setTaskTitle(e.target.value)}
|
||||||
sx={{ width: '100%', fontSize: '16px' }}
|
sx={{ width: '100%', fontSize: '16px' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box> */}
|
||||||
|
<Box>
|
||||||
{hasDescription ? (
|
{!hasDescription && (
|
||||||
<Box>
|
|
||||||
<Typography level='body-sm'>Description:</Typography>
|
|
||||||
<Textarea
|
|
||||||
minRows={2}
|
|
||||||
value={description}
|
|
||||||
onChange={e => setDescription(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Button
|
<Button
|
||||||
|
startDecorator={<Add />}
|
||||||
variant='plain'
|
variant='plain'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => setHasDescription(true)}
|
onClick={() => setHasDescription(true)}
|
||||||
>
|
>
|
||||||
Add Description
|
Description
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Typography level='body-sm'>Subtasks:</Typography>
|
{!hasSubTasks && (
|
||||||
<SubTasks editMode={true} tasks={subTasks} setTasks={setSubTasks} />
|
<Button
|
||||||
|
startDecorator={<Add />}
|
||||||
|
variant='plain'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => setHasSubTasks(true)}
|
||||||
|
>
|
||||||
|
Subtasks
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!dueDate && (
|
||||||
|
<Button
|
||||||
|
startDecorator={<Add />}
|
||||||
|
variant='plain'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => {
|
||||||
|
setDueDate(
|
||||||
|
moment().add(1, 'day').format('YYYY-MM-DDTHH:00:00'),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Due Date
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box
|
{hasDescription && (
|
||||||
sx={{
|
<Box>
|
||||||
marginTop: 2,
|
<Typography level='body-sm'>Description:</Typography>
|
||||||
display: 'flex',
|
<Textarea
|
||||||
flexDirection: 'row',
|
minRows={2}
|
||||||
gap: 2,
|
value={description}
|
||||||
}}
|
onChange={e => setDescription(e.target.value)}
|
||||||
>
|
/>
|
||||||
<FormControl>
|
</Box>
|
||||||
<Typography level='body-sm'>Priority</Typography>
|
)}
|
||||||
<Select
|
{hasSubTasks && (
|
||||||
defaultValue={0}
|
<Box>
|
||||||
value={priority}
|
<Typography level='body-sm'>Subtasks:</Typography>
|
||||||
onChange={(e, value) => setPriority(value)}
|
<SubTasks
|
||||||
>
|
editMode={true}
|
||||||
<Option value='0'>No Priority</Option>
|
tasks={subTasks ? subTasks : []}
|
||||||
<Option value='1'>P1</Option>
|
setTasks={setSubTasks}
|
||||||
<Option value='2'>P2</Option>
|
/>
|
||||||
<Option value='3'>P3</Option>
|
</Box>
|
||||||
<Option value='4'>P4</Option>
|
)}
|
||||||
</Select>
|
|
||||||
</FormControl>
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<Typography level='body-sm'>Priority</Typography>
|
||||||
|
<Select
|
||||||
|
defaultValue={0}
|
||||||
|
value={priority}
|
||||||
|
onChange={(e, value) => setPriority(value)}
|
||||||
|
>
|
||||||
|
<Option value='0'>No Priority</Option>
|
||||||
|
<Option value='1'>P1</Option>
|
||||||
|
<Option value='2'>P2</Option>
|
||||||
|
<Option value='3'>P3</Option>
|
||||||
|
<Option value='4'>P4</Option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
{dueDate && (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Typography level='body-sm'>Due Date</Typography>
|
<Typography level='body-sm'>Due Date</Typography>
|
||||||
<Input
|
<Input
|
||||||
@@ -646,55 +509,52 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
|||||||
sx={{ width: '100%', fontSize: '16px' }}
|
sx={{ width: '100%', fontSize: '16px' }}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Box>
|
)}
|
||||||
<Box
|
</Box>
|
||||||
sx={{
|
<Box
|
||||||
marginTop: 2,
|
sx={{
|
||||||
display: 'flex',
|
marginTop: 2,
|
||||||
flexDirection: 'row',
|
display: 'flex',
|
||||||
justifyContent: 'start',
|
flexDirection: 'row',
|
||||||
gap: 2,
|
justifyContent: 'start',
|
||||||
}}
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<Typography level='body-sm'>Assignee</Typography>
|
||||||
|
<Select value={'0'} disabled>
|
||||||
|
<Option value='0'>Me</Option>
|
||||||
|
{/* <Option value='1'>Other</Option> */}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Typography level='body-sm'>Frequency</Typography>
|
||||||
|
<Input value={frequencyHumanReadable || 'Once'} variant='plain' />
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'end',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
color='neutral'
|
||||||
|
onClick={handleCloseModal}
|
||||||
>
|
>
|
||||||
<FormControl>
|
Cancel
|
||||||
<Typography level='body-sm'>Assignee</Typography>
|
</Button>
|
||||||
<Select value={'0'} disabled>
|
<Button variant='solid' color='primary' onClick={handleSubmit}>
|
||||||
<Option value='0'>Me</Option>
|
Create
|
||||||
{/* <Option value='1'>Other</Option> */}
|
</Button>
|
||||||
</Select>
|
</Box>
|
||||||
</FormControl>
|
</ModalDialog>
|
||||||
<FormControl>
|
</ModalOverflow>
|
||||||
<Typography level='body-sm'>Frequency</Typography>
|
</Modal>
|
||||||
<Input
|
|
||||||
value={frequencyHumanReadable || 'Once'}
|
|
||||||
variant='plain'
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
marginTop: 2,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'end',
|
|
||||||
gap: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant='outlined'
|
|
||||||
color='neutral'
|
|
||||||
onClick={handleCloseModal}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button variant='solid' color='primary' onClick={handleSubmit}>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</ModalDialog>
|
|
||||||
</ModalOverflow>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
116
src/views/components/AutocompleteInput.jsx
Normal file
116
src/views/components/AutocompleteInput.jsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { Chip, List, ListItem, ListItemButton, Textarea } from '@mui/joy'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
const AutocompleteInput = ({ options, ref, value, onChange, ...props }) => {
|
||||||
|
const [filteredOptions, setFilteredOptions] = useState([])
|
||||||
|
const [menuVisible, setMenuVisible] = useState(false)
|
||||||
|
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
||||||
|
const [triggerKey, setTriggerKey] = useState(null)
|
||||||
|
// const inputRef = useRef(null)
|
||||||
|
const menuRef = useRef(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!triggerKey || !value.includes(triggerKey)) {
|
||||||
|
setMenuVisible(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = value.split(triggerKey).pop().toLowerCase()
|
||||||
|
const matchedOptions = (options[triggerKey] || []).filter(option =>
|
||||||
|
option.label.toLowerCase().startsWith(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
setFilteredOptions(matchedOptions)
|
||||||
|
setMenuVisible(matchedOptions.length > 0)
|
||||||
|
setHighlightedIndex(0)
|
||||||
|
}, [value, triggerKey, options])
|
||||||
|
|
||||||
|
const handleKeyDown = e => {
|
||||||
|
if (menuVisible) {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
setHighlightedIndex(prev => (prev + 1) % filteredOptions.length)
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
setHighlightedIndex(
|
||||||
|
prev => (prev - 1 + filteredOptions.length) % filteredOptions.length,
|
||||||
|
)
|
||||||
|
} else if (e.key === 'Tab' || e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (filteredOptions[highlightedIndex]) {
|
||||||
|
selectOption(filteredOptions[highlightedIndex])
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setMenuVisible(false)
|
||||||
|
}
|
||||||
|
} else if (Object.keys(options).includes(e.key)) {
|
||||||
|
setTriggerKey(e.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectOption = option => {
|
||||||
|
const parts = value.split(triggerKey)
|
||||||
|
parts.pop()
|
||||||
|
onChange(parts.join(triggerKey) + triggerKey + option.label + ' ')
|
||||||
|
setMenuVisible(false)
|
||||||
|
setTriggerKey(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickOutside = event => {
|
||||||
|
if (
|
||||||
|
menuRef.current &&
|
||||||
|
!menuRef.current.contains(event.target) &&
|
||||||
|
ref.current &&
|
||||||
|
!ref.current.contains(event.target)
|
||||||
|
) {
|
||||||
|
setMenuVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<Textarea
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder='Type here...'
|
||||||
|
/>
|
||||||
|
{menuVisible && (
|
||||||
|
<List ref={menuRef} style={{ position: 'absolute', zIndex: 1000 }}>
|
||||||
|
{filteredOptions.map((option, index) => (
|
||||||
|
<ListItem key={option.value || option.label}>
|
||||||
|
<ListItemButton
|
||||||
|
selected={index === highlightedIndex}
|
||||||
|
onClick={() => selectOption(option)}
|
||||||
|
>
|
||||||
|
{option.color && (
|
||||||
|
<Chip
|
||||||
|
style={{
|
||||||
|
backgroundColor: option.color,
|
||||||
|
marginRight: '0.5rem',
|
||||||
|
color: '#fff',
|
||||||
|
}}
|
||||||
|
size='sm'
|
||||||
|
variant='soft'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{option.label}
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutocompleteInput
|
||||||
@@ -21,15 +21,11 @@ const CalendarView = ({ chores }) => {
|
|||||||
|
|
||||||
const tileContent = ({ date, view }) => {
|
const tileContent = ({ date, view }) => {
|
||||||
if (view === 'month') {
|
if (view === 'month') {
|
||||||
const dayChores = chores.filter(chore => {
|
const dayChores = chores.filter(
|
||||||
// Validate chore.nextDueDate before using it
|
chore =>
|
||||||
if (!chore.nextDueDate) return false
|
new Date(chore.nextDueDate)?.toISOString().split('T')[0] ===
|
||||||
|
date.toISOString().split('T')[0],
|
||||||
const choreDate = new Date(chore.nextDueDate)
|
)
|
||||||
if (isNaN(choreDate)) return false // Check if the date is invalid
|
|
||||||
if (!date) return false
|
|
||||||
return choreDate.toLocaleDateString() === date.toLocaleDateString()
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='dot-container'>
|
<div className='dot-container'>
|
||||||
@@ -70,7 +66,7 @@ const CalendarView = ({ chores }) => {
|
|||||||
<Calendar
|
<Calendar
|
||||||
tileContent={tileContent}
|
tileContent={tileContent}
|
||||||
onChange={d => {
|
onChange={d => {
|
||||||
setSeletedDate(new Date(d.toLocaleDateString()))
|
setSeletedDate(new Date(d))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!selectedDate && (
|
{!selectedDate && (
|
||||||
@@ -122,8 +118,8 @@ const CalendarView = ({ chores }) => {
|
|||||||
{chores
|
{chores
|
||||||
.filter(
|
.filter(
|
||||||
chore =>
|
chore =>
|
||||||
new Date(chore.nextDueDate)?.toLocaleDateString() ===
|
new Date(chore.nextDueDate)?.toISOString().split('T')[0] ===
|
||||||
selectedDate.toLocaleDateString(),
|
selectedDate.toISOString().split('T')[0],
|
||||||
)
|
)
|
||||||
.map((chore, idx) => (
|
.map((chore, idx) => (
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
398
src/views/components/CustomParsers.js
Normal file
398
src/views/components/CustomParsers.js
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
const VALID_DAYS = {
|
||||||
|
monday: 'Monday',
|
||||||
|
mon: 'Monday',
|
||||||
|
tuesday: 'Tuesday',
|
||||||
|
tue: 'Tuesday',
|
||||||
|
wednesday: 'Wednesday',
|
||||||
|
wed: 'Wednesday',
|
||||||
|
thursday: 'Thursday',
|
||||||
|
thu: 'Thursday',
|
||||||
|
friday: 'Friday',
|
||||||
|
fri: 'Friday',
|
||||||
|
saturday: 'Saturday',
|
||||||
|
sat: 'Saturday',
|
||||||
|
sunday: 'Sunday',
|
||||||
|
sun: 'Sunday',
|
||||||
|
}
|
||||||
|
|
||||||
|
const VALID_MONTHS = {
|
||||||
|
january: 'January',
|
||||||
|
jan: 'January',
|
||||||
|
february: 'February',
|
||||||
|
feb: 'February',
|
||||||
|
march: 'March',
|
||||||
|
mar: 'March',
|
||||||
|
april: 'April',
|
||||||
|
apr: 'April',
|
||||||
|
may: 'May',
|
||||||
|
june: 'June',
|
||||||
|
jun: 'June',
|
||||||
|
july: 'July',
|
||||||
|
jul: 'July',
|
||||||
|
august: 'August',
|
||||||
|
aug: 'August',
|
||||||
|
september: 'September',
|
||||||
|
sep: 'September',
|
||||||
|
october: 'October',
|
||||||
|
oct: 'October',
|
||||||
|
november: 'November',
|
||||||
|
nov: 'November',
|
||||||
|
december: 'December',
|
||||||
|
dec: 'December',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL_MONTHS = Object.values(VALID_MONTHS).filter(
|
||||||
|
(v, i, a) => a.indexOf(v) === i,
|
||||||
|
)
|
||||||
|
|
||||||
|
export const parsePriority = inputSentence => {
|
||||||
|
let sentence = inputSentence.toLowerCase()
|
||||||
|
const priorityMap = {
|
||||||
|
1: ['p1', 'priority 1', 'high priority', 'urgent', 'asap', 'important'],
|
||||||
|
2: ['p2', 'priority 2', 'medium priority'],
|
||||||
|
3: ['p3', 'priority 3', 'low priority'],
|
||||||
|
4: ['p4', 'priority 4'],
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [priority, terms] of Object.entries(priorityMap)) {
|
||||||
|
if (terms.some(term => sentence.includes(term))) {
|
||||||
|
return {
|
||||||
|
result: priority,
|
||||||
|
highlight: terms
|
||||||
|
.map(term => {
|
||||||
|
const index = sentence.indexOf(term)
|
||||||
|
return {
|
||||||
|
text: term,
|
||||||
|
start: index,
|
||||||
|
end: index + term.length,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(term => term.start !== -1),
|
||||||
|
|
||||||
|
cleanedSentence: terms.reduce(
|
||||||
|
(s, t) => s.replace(t, ''),
|
||||||
|
inputSentence,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { result: 0, cleanedSentence: inputSentence }
|
||||||
|
}
|
||||||
|
export const parseLabels = (inputSentence, userLabels) => {
|
||||||
|
let sentence = inputSentence.toLowerCase()
|
||||||
|
const currentLabels = []
|
||||||
|
// label will always be prefixed #:
|
||||||
|
|
||||||
|
for (const label of userLabels) {
|
||||||
|
if (sentence.includes(`#${label.name.toLowerCase()}`)) {
|
||||||
|
currentLabels.push(label)
|
||||||
|
sentence = sentence.replace(`#${label.name.toLowerCase()}`, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentLabels.length > 0) {
|
||||||
|
return {
|
||||||
|
result: currentLabels,
|
||||||
|
highlight: currentLabels.map(label => {
|
||||||
|
const index = inputSentence
|
||||||
|
.toLowerCase()
|
||||||
|
.indexOf(`#${label.name.toLowerCase()}`)
|
||||||
|
return {
|
||||||
|
text: `#${label.name}`,
|
||||||
|
start: index,
|
||||||
|
end: index + label.name.length + 1,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
cleanedSentence: sentence,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { result: null, cleanedSentence: sentence }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseRepeatV2 = inputSentence => {
|
||||||
|
const sentence = inputSentence.toLowerCase()
|
||||||
|
const result = {
|
||||||
|
frequency: 1,
|
||||||
|
frequencyType: null,
|
||||||
|
frequencyMetadata: {
|
||||||
|
days: [],
|
||||||
|
months: [],
|
||||||
|
unit: null,
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const patterns = [
|
||||||
|
{
|
||||||
|
frequencyType: 'day_of_the_month:every',
|
||||||
|
regex: /(\d+)(?:th|st|nd|rd)? of every month/i,
|
||||||
|
name: 'Every {day} of every month',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'daily',
|
||||||
|
regex: /(every day|daily|everyday)/i,
|
||||||
|
name: 'Every day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'daily:time',
|
||||||
|
regex: /every (morning|noon|afternoon|evening|night)/i,
|
||||||
|
name: 'Every {time} daily',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'weekly',
|
||||||
|
regex: /(every week|weekly)/i,
|
||||||
|
name: 'Every week',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'monthly',
|
||||||
|
regex: /(every month|monthly)/i,
|
||||||
|
name: 'Every month',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'yearly',
|
||||||
|
regex: /every year/i,
|
||||||
|
name: 'Every year',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'monthly',
|
||||||
|
regex: /every (?:other )?month/i,
|
||||||
|
name: 'Bi Monthly',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'interval:2week',
|
||||||
|
regex: /(bi-?weekly|every other week)/i,
|
||||||
|
value: 2,
|
||||||
|
name: 'Bi Weekly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'interval',
|
||||||
|
regex: /every (\d+) (days?|weeks?|months?|years?)/i,
|
||||||
|
name: 'Every {frequency} {unit}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'interval:every_other',
|
||||||
|
regex: /every other (days?|weeks?|months?|years?)/i,
|
||||||
|
name: 'Every other {unit}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'days_of_the_week',
|
||||||
|
regex: /every ([\w, ]+(?:day)?(?:, [\w, ]+(?:day)?)*)/i,
|
||||||
|
name: 'Every {days}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'day_of_the_month',
|
||||||
|
regex: /(\d+)(?:st|nd|rd|th)? of ([\w ]+(?:(?:,| and |\s)[\w ]+)*)/i,
|
||||||
|
name: 'Every {day} days of {months}',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = sentence.match(pattern.regex)
|
||||||
|
if (!match) continue
|
||||||
|
|
||||||
|
result.frequencyType = pattern.frequencyType
|
||||||
|
const unitMap = {
|
||||||
|
daily: 'days',
|
||||||
|
weekly: 'weeks',
|
||||||
|
monthly: 'months',
|
||||||
|
yearly: 'years',
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (pattern.frequencyType) {
|
||||||
|
case 'daily':
|
||||||
|
case 'weekly':
|
||||||
|
case 'monthly':
|
||||||
|
case 'yearly':
|
||||||
|
result.frequencyType = 'interval'
|
||||||
|
result.frequency = pattern.value || 1
|
||||||
|
result.frequencyMetadata.unit = unitMap[pattern.frequencyType]
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name,
|
||||||
|
highlight: [
|
||||||
|
{
|
||||||
|
text: pattern.name,
|
||||||
|
start: inputSentence.indexOf(match[0]),
|
||||||
|
end: inputSentence.indexOf(match[0]) + match[0].length,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'interval':
|
||||||
|
result.frequency = parseInt(match[1], 10)
|
||||||
|
result.frequencyMetadata.unit = match[2]
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name
|
||||||
|
.replace('{frequency}', result.frequency)
|
||||||
|
.replace('{unit}', result.frequencyMetadata.unit),
|
||||||
|
highlight: [
|
||||||
|
{
|
||||||
|
text: pattern.name,
|
||||||
|
start: inputSentence.indexOf(match[0]),
|
||||||
|
end: inputSentence.indexOf(match[0]) + match[0].length,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'days_of_the_week':
|
||||||
|
result.frequencyMetadata.days = match[1]
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/ and |,|\s/)
|
||||||
|
.map(day => day.trim())
|
||||||
|
.filter(day => VALID_DAYS[day])
|
||||||
|
.map(day => VALID_DAYS[day])
|
||||||
|
if (!result.frequencyMetadata.days.length)
|
||||||
|
return { result: null, name: null, cleanedSentence: inputSentence }
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name.replace(
|
||||||
|
'{days}',
|
||||||
|
result.frequencyMetadata.days.join(', '),
|
||||||
|
),
|
||||||
|
highlight: [
|
||||||
|
{
|
||||||
|
text: pattern.name,
|
||||||
|
start: inputSentence.indexOf(match[0]),
|
||||||
|
end: inputSentence.indexOf(match[0]) + match[0].length,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'day_of_the_month':
|
||||||
|
result.frequency = parseInt(match[1], 10)
|
||||||
|
result.frequencyMetadata.months = match[2]
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/ and |,|\s/)
|
||||||
|
.map(month => month.trim())
|
||||||
|
.filter(month => VALID_MONTHS[month])
|
||||||
|
.map(month => VALID_MONTHS[month])
|
||||||
|
result.frequencyMetadata.unit = 'days'
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name
|
||||||
|
.replace('{day}', result.frequency)
|
||||||
|
.replace('{months}', result.frequencyMetadata.months.join(', ')),
|
||||||
|
highlight: [
|
||||||
|
{
|
||||||
|
text: pattern.name,
|
||||||
|
start: inputSentence.indexOf(match[0]),
|
||||||
|
end: inputSentence.indexOf(match[0]) + match[0].length,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
case 'interval:2week':
|
||||||
|
result.frequency = 2
|
||||||
|
result.frequencyMetadata.unit = 'weeks'
|
||||||
|
result.frequencyType = 'interval'
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name,
|
||||||
|
highlight: [
|
||||||
|
{
|
||||||
|
text: pattern.name,
|
||||||
|
start: inputSentence.indexOf(match[0]),
|
||||||
|
end: inputSentence.indexOf(match[0]) + match[0].length,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
case 'daily:time':
|
||||||
|
result.frequency = 1
|
||||||
|
result.frequencyMetadata.unit = 'days'
|
||||||
|
result.frequencyType = 'daily'
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name.replace('{time}', match[1]),
|
||||||
|
// replace every x with ''
|
||||||
|
highlight: [
|
||||||
|
{
|
||||||
|
text: pattern.name,
|
||||||
|
start: inputSentence.indexOf(match[0]),
|
||||||
|
end: inputSentence.indexOf(match[0]) + match[0].length,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'day_of_the_month:every':
|
||||||
|
result.frequency = parseInt(match[1], 10)
|
||||||
|
result.frequencyMetadata.months = ALL_MONTHS
|
||||||
|
result.frequencyMetadata.unit = 'days'
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name
|
||||||
|
.replace('{day}', result.frequency)
|
||||||
|
.replace('{months}', result.frequencyMetadata.months.join(', ')),
|
||||||
|
highlight: [
|
||||||
|
{
|
||||||
|
text: pattern.name,
|
||||||
|
start: inputSentence.indexOf(match[0]),
|
||||||
|
end: inputSentence.indexOf(match[0]) + match[0].length,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
case 'interval:every_other':
|
||||||
|
result.frequency = 2
|
||||||
|
result.frequencyMetadata.unit = match[1]
|
||||||
|
result.frequencyType = 'interval'
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name.replace('{unit}', result.frequencyMetadata.unit),
|
||||||
|
highlight: [
|
||||||
|
{
|
||||||
|
text: pattern.name,
|
||||||
|
start: inputSentence.indexOf(match[0]),
|
||||||
|
end: inputSentence.indexOf(match[0]) + match[0].length,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
result: null,
|
||||||
|
name: null,
|
||||||
|
highlight: [],
|
||||||
|
cleanedSentence: inputSentence,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseAssignees = (inputSentence, users) => {
|
||||||
|
const sentence = inputSentence.toLowerCase()
|
||||||
|
const result = []
|
||||||
|
const highlight = []
|
||||||
|
console.log('users:', users)
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
if (sentence.includes(`@${user.username.toLowerCase()}`)) {
|
||||||
|
result.push(user)
|
||||||
|
const index = inputSentence
|
||||||
|
.toLowerCase()
|
||||||
|
.indexOf(`@${user.username.toLowerCase()}`)
|
||||||
|
highlight.push({
|
||||||
|
text: `@${user.username}`,
|
||||||
|
start: index,
|
||||||
|
end: index + user.username.length + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
highlight,
|
||||||
|
cleanedSentence: sentence.replace(
|
||||||
|
new RegExp(`@(${users.map(u => u.username).join('|')})`, 'g'),
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { result: null, cleanedSentence: sentence }
|
||||||
|
}
|
||||||
0
src/views/components/ErrorSnackbar.jsx
Normal file
0
src/views/components/ErrorSnackbar.jsx
Normal file
@@ -1,5 +1,5 @@
|
|||||||
import { Info } from '@mui/icons-material'
|
import { Info } from '@mui/icons-material'
|
||||||
import { Box, Button, Sheet } from '@mui/joy'
|
import { Box, IconButton, Sheet } from '@mui/joy'
|
||||||
import React, { useRef, useState } from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
|
|
||||||
const LearnMoreButton = ({ content }) => {
|
const LearnMoreButton = ({ content }) => {
|
||||||
@@ -29,16 +29,15 @@ const LearnMoreButton = ({ content }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: 'relative', display: 'inline-block' }}>
|
<Box sx={{ position: 'relative', display: 'inline-block' }}>
|
||||||
<Button
|
<IconButton
|
||||||
ref={anchorRef}
|
ref={anchorRef}
|
||||||
variant='plain'
|
variant='plain'
|
||||||
startDecorator={<Info />}
|
|
||||||
size='sm'
|
size='sm'
|
||||||
color='primary'
|
color='primary'
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
>
|
>
|
||||||
Learn More
|
<Info />
|
||||||
</Button>
|
</IconButton>
|
||||||
{open && (
|
{open && (
|
||||||
<Sheet
|
<Sheet
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
|
|||||||
Reference in New Issue
Block a user