Compare commits

..

19 commits

Author SHA1 Message Date
6bba18e208
feat: discord notifications & notify by default
Some checks are pending
Build validation / build (20.x) (push) Waiting to run
Build validation / build (22.x) (push) Waiting to run
Build validation / lint (push) Waiting to run
2025-04-04 23:32:59 +01:00
Mo Tarbin
36faaf0b97 Merge branch 'dev' 2025-03-17 01:03:05 -04:00
Mo Tarbin
e56893fd64 Add frequency and notification metadata to task input; update NavBar styles for improved layout 2025-03-17 01:02:52 -04:00
Mo Tarbin
c553e2077a Merge branch 'main' of https://github.com/Donetick/frontend 2025-03-05 22:06:09 -05:00
Mo Tarbin
76ed927839 Merge branch 'dev' 2025-03-05 22:04:36 -05:00
Mo Tarbin
08de84a889 Fix optional chaining for userProfile in chore filters and adjust data destructuring in MyChores component 2025-03-05 22:04:28 -05:00
Mo Tarbin
52b81cc115 Refactor MyChores component to include chore filters in ChoresGrouper calls 2025-03-05 21:41:32 -05:00
Mo Tarbin
de08c6c5b1 Enhance ChoresGrouper to include chore filters and update localStorage handling for selectedChoreFilter 2025-03-05 21:40:47 -05:00
Mo Tarbin
ee566bd2a3 Merge branch 'dev' 2025-03-05 21:06:32 -05:00
Mo Tarbin
9d8aec1aa6 Fix localStorage retrieval for selectedChoreFilter in MyChores component 2025-03-05 21:06:22 -05:00
Mohamad Tarbin
9dc8fe77bc
Update README.md 2025-03-05 19:49:00 -05:00
Mohamad Tarbin
409851a64c
Merge pull request #18 from ukr01/feature/SSO-Entropy-Fix
Adds 8 chars to state in call to custom auth provider instead of 6
2025-03-05 19:46:23 -05:00
Mo Tarbin
cd1b556fb1 Merge branch 'dev' 2025-03-05 19:45:38 -05:00
Mo Tarbin
467fc935f0 - Support nest sub tasks
- Support filters in ChoreGrouper
- completion window only available if due date selected
- Add SortAndGrouping Component : support Filter by Assignee
- update Notification Switch to align left of the text
- Support caching filters
2025-03-05 19:43:43 -05:00
Ulrik Kristensen
a98f9c698b fixes randomState to have 8 chars instead of 6 2025-03-03 13:20:08 +01:00
Mo Tarbin
6431e7145d Bump version to 0.1.95 and add dnd-kit dependencies; implement CompleteSubTask function and enhance chore sorting logic 2025-02-25 23:40:26 -05:00
Mo Tarbin
47348190ab Merge branch 'dev' 2025-02-15 00:05:51 -05:00
Mo Tarbin
cbf99fad18 Fix https://github.com/donetick/donetick/issues/128 2025-02-15 00:05:44 -05:00
Mo Tarbin
c06d5f1f30 Merge branch 'main' of https://github.com/meauxt/donetick-frontend 2025-02-12 22:26:32 -05:00
11 changed files with 200 additions and 27 deletions

19
.direnv/bin/nix-direnv-reload Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
if [[ ! -d "/home/amy/code/public/oss/frontend" ]]; then
echo "Cannot find source directory; Did you move it?"
echo "(Looking for "/home/amy/code/public/oss/frontend")"
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
exit 1
fi
# rebuild the cache forcefully
_nix_direnv_force_reload=1 direnv exec "/home/amy/code/public/oss/frontend" true
# Update the mtime for .envrc.
# This will cause direnv to reload again - but without re-building.
touch "/home/amy/code/public/oss/frontend/.envrc"
# Also update the timestamp of whatever profile_rc we have.
# This makes sure that we know we are up to date.
touch -r "/home/amy/code/public/oss/frontend/.envrc" "/home/amy/code/public/oss/frontend/.direnv"/*.rc

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

View file

@ -49,4 +49,4 @@ While maintaining Donetick's commitment to open source, this hosted option will
## License ## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. I might consider changing it later to something else This project is licensed under the AGPLv3 License. See the [LICENSE](LICENSE) file for more details. I might consider changing it later to something else

61
flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1743583204,
"narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1743296961,
"narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

23
flake.nix Normal file
View file

@ -0,0 +1,23 @@
{
description = "Description for the project";
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
perSystem = { config, self', inputs', pkgs, system, ... }: {
devShells.default = pkgs.mkShell {
packages = with pkgs; [
nodePackages.npm
nodejs
];
};
};
flake = {
};
};
}

View file

@ -179,9 +179,9 @@ export const notInCompletionWindow = chore => {
export const ChoreFilters = userProfile => ({ export const ChoreFilters = userProfile => ({
anyone: () => true, anyone: () => true,
assigned_to_me: chore => { assigned_to_me: chore => {
return chore.assignedTo && chore.assignedTo === userProfile.id return chore.assignedTo && chore.assignedTo === userProfile?.id
}, },
assigned_to_others: chore => { assigned_to_others: chore => {
return chore.assignedTo && chore.assignedTo !== userProfile.id return chore.assignedTo && chore.assignedTo !== userProfile?.id
}, },
}) })

View file

@ -115,7 +115,7 @@ const LoginView = () => {
Navigate('/forgot-password') Navigate('/forgot-password')
} }
const generateRandomState = () => { const generateRandomState = () => {
const randomState = Math.random().toString(36).substring(7) const randomState = Math.random().toString(32).substring(5)
localStorage.setItem('authState', randomState) localStorage.setItem('authState', randomState)
return randomState return randomState

View file

@ -74,7 +74,7 @@ const MyChores = () => {
JSON.parse(localStorage.getItem('openChoreSections')) || {}, JSON.parse(localStorage.getItem('openChoreSections')) || {},
) )
const [selectedChoreFilter, setSelectedChoreFilter] = useState( const [selectedChoreFilter, setSelectedChoreFilter] = useState(
JSON.parse(localStorage.getItem('selectedChoreFilter')) || 'anyone', localStorage.getItem('selectedChoreFilter') || 'anyone',
) )
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
const [performers, setPerformers] = useState([]) const [performers, setPerformers] = useState([])
@ -98,13 +98,12 @@ const MyChores = () => {
throw new Error(userProfileResponse.statusText) throw new Error(userProfileResponse.statusText)
} }
Promise.all([ Promise.all([
userProfileResponse.json(),
choresResponse.json(), choresResponse.json(),
usersResponse.json(), usersResponse.json(),
userProfileResponse.json(),
]).then(data => { ]).then(data => {
const [choresData, usersData, userProfileData] = data const [userProfileData, choresData, usersData] = data
setUserProfile(userProfileData.res) setUserProfile(userProfileData.res)
choresData.res.sort(ChoreSorter)
setChores(choresData.res) setChores(choresData.res)
setFilteredChores(choresData.res) setFilteredChores(choresData.res)
setPerformers(usersData.res) setPerformers(usersData.res)
@ -139,7 +138,11 @@ const MyChores = () => {
const sortedChores = choresData.res.sort(ChoreSorter) const sortedChores = choresData.res.sort(ChoreSorter)
setChores(sortedChores) setChores(sortedChores)
setFilteredChores(sortedChores) setFilteredChores(sortedChores)
const sections = ChoresGrouper(selectedChoreSection, sortedChores) const sections = ChoresGrouper(
selectedChoreSection,
sortedChores,
ChoreFilters(userProfile)[selectedChoreFilter],
)
setChoreSections(sections) setChoreSections(sections)
if (localStorage.getItem('openChoreSections') === null) { if (localStorage.getItem('openChoreSections') === null) {
setSelectedChoreSectionWithCache(selectedChoreSection) setSelectedChoreSectionWithCache(selectedChoreSection)
@ -178,7 +181,7 @@ const MyChores = () => {
} }
const setSelectedChoreFilterWithCache = value => { const setSelectedChoreFilterWithCache = value => {
setSelectedChoreFilter(value) setSelectedChoreFilter(value)
localStorage.setItem('selectedChoreFilter', JSON.stringify(value)) localStorage.setItem('selectedChoreFilter', value)
} }
const updateChores = newChore => { const updateChores = newChore => {
@ -186,7 +189,13 @@ const MyChores = () => {
newChores.push(newChore) newChores.push(newChore)
setChores(newChores) setChores(newChores)
setFilteredChores(newChores) setFilteredChores(newChores)
setChoreSections(ChoresGrouper(selectedChoreSection, newChores)) setChoreSections(
ChoresGrouper(
selectedChoreSection,
newChores,
ChoreFilters(userProfile)[selectedChoreFilter],
),
)
setSearchFilter('All') setSearchFilter('All')
} }
const handleMenuOutsideClick = event => { const handleMenuOutsideClick = event => {
@ -262,7 +271,13 @@ const MyChores = () => {
} }
setChores(newChores) setChores(newChores)
setFilteredChores(newFilteredChores) setFilteredChores(newFilteredChores)
setChoreSections(ChoresGrouper(selectedChoreSection, newChores)) setChoreSections(
ChoresGrouper(
selectedChoreSection,
newChores,
ChoreFilters(userProfile)[selectedChoreFilter],
),
)
switch (event) { switch (event) {
case 'completed': case 'completed':
@ -293,7 +308,13 @@ const MyChores = () => {
) )
setChores(newChores) setChores(newChores)
setFilteredChores(newFilteredChores) setFilteredChores(newFilteredChores)
setChoreSections(ChoresGrouper(selectedChoreSection, newChores)) setChoreSections(
ChoresGrouper(
selectedChoreSection,
newChores,
ChoreFilters(userProfile)[selectedChoreFilter],
),
)
} }
const searchOptions = { const searchOptions = {
@ -449,7 +470,11 @@ const MyChores = () => {
) )
}} }}
onItemSelect={selected => { onItemSelect={selected => {
const section = ChoresGrouper(selected.value, chores) const section = ChoresGrouper(
selected.value,
chores,
ChoreFilters(userProfile)[selectedChoreFilter],
)
setChoreSections(section) setChoreSections(section)
setSelectedChoreSectionWithCache(selected.value) setSelectedChoreSectionWithCache(selected.value)
setOpenChoreSectionsWithCache( setOpenChoreSectionsWithCache(

View file

@ -98,6 +98,10 @@ const NotificationSetting = () => {
const [chatID, setChatID] = useState( const [chatID, setChatID] = useState(
userProfile?.notification_target?.target_id, userProfile?.notification_target?.target_id,
) )
const [webhookURL, setWebhookURL] = useState(
userProfile?.notification_target?.target_id,
)
const [error, setError] = useState('') const [error, setError] = useState('')
const SaveValidation = () => { const SaveValidation = () => {
switch (notificationTarget) { switch (notificationTarget) {
@ -116,6 +120,12 @@ const NotificationSetting = () => {
return false return false
} }
break break
case '4':
if (webhookURL === '') {
setError('Webhook URL is required')
return false
}
break
default: default:
break break
} }
@ -126,7 +136,8 @@ const NotificationSetting = () => {
if (!SaveValidation()) return if (!SaveValidation()) return
UpdateNotificationTarget({ UpdateNotificationTarget({
target: chatID, // 4 = Discord
target: notificationTarget === '4' ? webhookURL : chatID,
type: Number(notificationTarget), type: Number(notificationTarget),
}).then(resp => { }).then(resp => {
if (resp.status != 200) { if (resp.status != 200) {
@ -134,13 +145,26 @@ const NotificationSetting = () => {
return return
} }
setUserProfile({ // Discord
...userProfile, if (notificationTarget === '4') {
notification_target: { setUserProfile({
target: chatID, ...userProfile,
type: Number(notificationTarget), notification_target: {
}, target: webhookURL,
}) type: Number(notificationTarget),
},
})
} else {
// Others (Telegram)
setUserProfile({
...userProfile,
notification_target: {
target: chatID,
type: Number(notificationTarget),
},
})
}
alert('Notification target updated') alert('Notification target updated')
}) })
} }
@ -323,7 +347,7 @@ const NotificationSetting = () => {
<Typography level='h3'>Custom Notification</Typography> <Typography level='h3'>Custom Notification</Typography>
<Divider /> <Divider />
<Typography level='body-md'> <Typography level='body-md'>
Notificaiton through other platform like Telegram or Pushover Notification through other platform like Telegram or Pushover
</Typography> </Typography>
<FormControl orientation='horizontal'> <FormControl orientation='horizontal'>
@ -380,6 +404,7 @@ const NotificationSetting = () => {
<Option value='0'>None</Option> <Option value='0'>None</Option>
<Option value='1'>Telegram</Option> <Option value='1'>Telegram</Option>
<Option value='2'>Pushover</Option> <Option value='2'>Pushover</Option>
<Option value='4'>Discord</Option>
<Option value='3'>Webhooks</Option> <Option value='3'>Webhooks</Option>
</Select> </Select>
{notificationTarget === '1' && ( {notificationTarget === '1' && (
@ -438,6 +463,19 @@ const NotificationSetting = () => {
/> />
</> </>
)} )}
{notificationTarget === '4' && (
<>
<Typography level='body-sm'>Webhook URL</Typography>
<Input
value={webhookURL}
onChange={e => setWebhookURL(e.target.value)}
placeholder='Webhook URL'
sx={{
width: '200px',
}}
/>
</>
)}
{error && ( {error && (
<Typography color='warning' level='body-sm'> <Typography color='warning' level='body-sm'>
{error} {error}

View file

@ -430,12 +430,18 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
assignedTo: userProfile.id, assignedTo: userProfile.id,
assignStrategy: 'random', assignStrategy: 'random',
isRolling: false, isRolling: false,
notification: false, notification: true,
description: description || null, description: description || null,
labelsV2: [], labelsV2: [],
priority: priority || 0, priority: priority || 0,
status: 0, status: 0,
frequencyType: 'once', frequencyType: 'once',
frequencyMetadata: {},
notificationMetadata: {
dueDate: true,
predue: true,
nagging: true
},
} }
if (frequency) { if (frequency) {

View file

@ -105,12 +105,12 @@ const NavBar = () => {
} }
return ( return (
<nav className='flex gap-2 p-2'> <nav className='flex gap-2 p-3'>
<IconButton size='sm' variant='plain' onClick={() => setDrawerOpen(true)}> <IconButton size='md' variant='plain' onClick={() => setDrawerOpen(true)}>
<MenuRounded /> <MenuRounded />
</IconButton> </IconButton>
<Box <Box
className='flex items-center gap-1' className='flex items-center gap-2'
onClick={() => { onClick={() => {
navigate('/my/chores') navigate('/my/chores')
}} }}