diff --git a/src/components/UserProfileAvatar.jsx b/src/components/UserProfileAvatar.jsx
index 0b31008..4065356 100644
--- a/src/components/UserProfileAvatar.jsx
+++ b/src/components/UserProfileAvatar.jsx
@@ -31,6 +31,7 @@ import { useNavigate } from 'react-router-dom'
import { useImpersonateUser } from '../contexts/ImpersonateUserContext'
import useStickyState from '../hooks/useStickyState'
import { useCircleMembers, useUserProfile } from '../queries/UserQueries'
+import { apiClient } from '../utils/apiClient'
import { isPlusAccount } from '../utils/Helpers'
import UserModal from '../views/Modals/Inputs/UserModal'
import SubscriptionModal from './SubscriptionModal'
@@ -76,9 +77,7 @@ const UserProfileAvatar = () => {
}
const handleLogout = () => {
- localStorage.removeItem('access_token')
- localStorage.removeItem('ca_expiration')
- window.location.href = '/login'
+ apiClient.handleLogout()
}
const handleSupportEmail = () => {
diff --git a/src/components/common/BottomSheetModal.jsx b/src/components/common/BottomSheetModal.jsx
index 51a3b12..029475d 100644
--- a/src/components/common/BottomSheetModal.jsx
+++ b/src/components/common/BottomSheetModal.jsx
@@ -1,5 +1,5 @@
import { Close } from '@mui/icons-material'
-import { IconButton, Modal, Sheet, Typography } from '@mui/joy'
+import { Divider, IconButton, Modal, Sheet, Typography } from '@mui/joy'
import { forwardRef, useEffect, useState } from 'react'
import { Z_INDEX } from '../../constants/zIndex'
@@ -10,6 +10,7 @@ const BottomSheetModal = forwardRef(
onClose,
children,
title,
+ footer,
height = 'auto',
maxHeight = '90vh',
expandedHeight = '95vh',
@@ -79,7 +80,11 @@ const BottomSheetModal = forwardRef(
const currentHeight = isExpanded ? expandedHeight : height
// Filter out DOM props that shouldn't be passed to Modal
- const { fullWidth: _fullWidth, unmountDelay: _unmountDelay, ...modalProps } = props
+ const {
+ fullWidth: _fullWidth,
+ unmountDelay: _unmountDelay,
+ ...modalProps
+ } = props
return (
{children}
+
+ {footer && (
+ <>
+
+
+ >
+ )}
)
diff --git a/src/hooks/useAuth.jsx b/src/hooks/useAuth.jsx
index 4dc41b8..3551708 100644
--- a/src/hooks/useAuth.jsx
+++ b/src/hooks/useAuth.jsx
@@ -2,6 +2,7 @@ import { createContext, useContext, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { API_URL } from '../Config'
import { apiClient } from '../utils/ApiClient'
+import { saveTokens, clearAllTokens } from '../utils/TokenStorage'
const AuthContext = createContext(null)
@@ -28,14 +29,10 @@ export const AuthProvider = ({ children }) => {
return new Date() >= new Date(expiry)
}
- const clearAuth = () => {
+ const clearAuth = async () => {
setToken(null)
setUser(null)
- localStorage.removeItem('token')
- localStorage.removeItem('token_expiry')
- localStorage.removeItem('ca_token')
- localStorage.removeItem('ca_expiration')
- localStorage.removeItem('access_token')
+ await clearAllTokens()
}
const login = async credentials => {
@@ -58,14 +55,14 @@ export const AuthProvider = ({ children }) => {
if (userToken) {
setToken(userToken)
- localStorage.setItem('token', userToken)
- if (data.expire || data.access_token_expiry) {
- localStorage.setItem(
- 'token_expiry',
- data.expire || data.access_token_expiry,
- )
- }
+ // Use centralized token storage
+ await saveTokens({
+ accessToken: userToken,
+ accessTokenExpiry: data.expire || data.access_token_expiry,
+ refreshToken: data.refresh_token,
+ refreshTokenExpiry: data.refresh_token_expiry,
+ })
}
setIsLoading(false)
@@ -86,7 +83,7 @@ export const AuthProvider = ({ children }) => {
} catch (error) {
console.warn('Logout API call failed:', error)
} finally {
- clearAuth()
+ await clearAuth()
setIsLoading(false)
navigate('/login')
}
@@ -112,22 +109,21 @@ export const AuthProvider = ({ children }) => {
}
useEffect(() => {
- const initAuth = async () => {
- if (token && !isTokenExpired()) {
- await fetchUser()
- } else if (token && isTokenExpired()) {
- // Token is expired, but don't refresh here
- // Let the first API call handle refresh via ApiClient
- // Just try to fetch user - if it fails, ApiClient will handle refresh
- await fetchUser()
- } else {
- clearAuth()
- navigate('/login')
- }
- setIsLoading(false)
- }
-
- initAuth()
+ // const initAuth = async () => {
+ // if (token && !isTokenExpired()) {
+ // await fetchUser()
+ // } else if (token && isTokenExpired()) {
+ // // Token is expired, but don't refresh here
+ // // Let the first API call handle refresh via ApiClient
+ // // Just try to fetch user - if it fails, ApiClient will handle refresh
+ // await fetchUser()
+ // } else {
+ // clearAuth()
+ // navigate('/login')
+ // }
+ // setIsLoading(false)
+ // }
+ // initAuth()
}, [token, navigate])
const value = {
diff --git a/src/hooks/useSSE.js b/src/hooks/useSSE.js
index 2729d06..5b21273 100644
--- a/src/hooks/useSSE.js
+++ b/src/hooks/useSSE.js
@@ -443,6 +443,13 @@ export const useSSE = () => {
return // Exit early, don't use exponential backoff for 401 errors
} else {
+ // Check if refresh token expired
+ if (refreshResult.error === 'Refresh token expired') {
+ console.error('Refresh token expired, user must login again')
+ setError('Session expired - please log in again')
+ return // Don't attempt reconnection
+ }
+
console.error('Token refresh failed:', refreshResult.error)
setError('Authentication failed - please log in again')
// Don't attempt reconnection if token refresh failed
diff --git a/src/queries/UserQueries.jsx b/src/queries/UserQueries.jsx
index c9a97d8..9ffe727 100644
--- a/src/queries/UserQueries.jsx
+++ b/src/queries/UserQueries.jsx
@@ -46,24 +46,11 @@ export const useUserProfile = () => {
const { data, error, isLoading } = useQuery({
queryKey: ['userProfile'],
queryFn: async () => {
- // the below code deleted because it cause issue when token expire and screen is off
- // and then user comes back to the app after long time. the user profile fetch would fail
- // and there is no retry for some reason. remove this seem to fix the issue.
-
- if (!isTokenValid()) {
- throw new Error('Invalid or expired token, cannot fetch user profile')
- }
const resp = await GetUserProfile()
const result = await resp.json()
// if we got 403 then user probably deleted their account and token is still valid. navigate to login
- if (resp.status === 403) {
- localStorage.removeItem('access_token')
- localStorage.removeItem('ca_expiration')
- window.location.href = '/login'
- throw new Error('User account deleted or access forbidden')
- }
- return result.res // Return the actual user profile data
+ return result.res || null
},
staleTime: 30 * 60 * 1000, // 30 minutes in milliseconds
gcTime: 30 * 60 * 1000, // 30 minutes in milliseconds
diff --git a/src/service/NotificationProvider.jsx b/src/service/NotificationProvider.jsx
index 9d6dc39..2a736a2 100644
--- a/src/service/NotificationProvider.jsx
+++ b/src/service/NotificationProvider.jsx
@@ -1,4 +1,4 @@
-import { CheckCircle, Error, Info, Warning } from '@mui/icons-material'
+import { CheckCircle, Error, Info, Undo, Warning } from '@mui/icons-material'
import { Box, Button, Snackbar, Typography } from '@mui/joy'
import React, { createContext, useContext, useState } from 'react'
@@ -24,10 +24,17 @@ const NOTIFICATION_TYPES = {
success: {
color: 'success',
icon: ,
- autoHideDuration: 3000,
+ autoHideDuration: 5000,
showDismissButton: false,
defaultTitle: 'Success',
},
+ undo: {
+ color: 'success',
+ icon: ,
+ autoHideDuration: null,
+ showDismissButton: false,
+ defaultTitle: 'Undone Successfully',
+ },
warning: {
color: 'warning',
icon: ,
@@ -134,6 +141,10 @@ export const NotificationProvider = ({ children }) => {
return addNotification(normalizeNotification(error, 'error'))
}
+ const showUndo = message => {
+ return addNotification(normalizeNotification(message, 'undo'))
+ }
+
const showSuccess = message => {
return addNotification(normalizeNotification(message, 'success'))
}
@@ -189,7 +200,18 @@ export const NotificationProvider = ({ children }) => {
onClose={() => removeNotification(notification.id)}
startDecorator={notificationIcon}
endDecorator={
- config.showDismissButton ? (
+ notification.undoAction ? (
+
+ ) : config.showDismissButton ? (
diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx
index 6b6923d..0a003c5 100644
--- a/src/views/ChoreEdit/ChoreView.jsx
+++ b/src/views/ChoreEdit/ChoreView.jsx
@@ -1,4 +1,5 @@
import {
+ Archive,
CalendarMonth,
CancelScheduleSend,
Check,
@@ -15,6 +16,7 @@ import {
SwitchAccessShortcut,
ThumbDown,
ThumbUp,
+ Unarchive,
} from '@mui/icons-material'
import {
Box,
@@ -60,6 +62,7 @@ import {
MarkChoreComplete,
RejectChore,
SkipChore,
+ UnArchiveChore,
UpdateChorePriority,
} from '../../utils/Fetcher'
import Priorities from '../../utils/Priorities'
@@ -354,6 +357,18 @@ const ChoreView = () => {
})
}
+ const handleUnarchiveChore = () => {
+ UnArchiveChore(choreId).then(response => {
+ if (response.ok) {
+ response.json().then(data => {
+ setChore({ ...chore, isActive: true })
+ // Invalidate chores cache to refetch data
+ queryClient.invalidateQueries(['chores'])
+ })
+ }
+ })
+ }
+
// Check if the current user can approve/reject (admin, manager, or task owner)
const canApproveReject = () => {
if (!circleMembersData?.res || !chore) return false
@@ -408,6 +423,16 @@ const ChoreView = () => {
>
{chore.name}
+ {chore.isActive === false && (
+ }
+ size='md'
+ color='warning'
+ sx={{ mb: 1 }}
+ >
+ Archived
+
+ )}
} size='md' sx={{ mb: 1 }}>
{chore.nextDueDate
? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}`
@@ -531,6 +556,7 @@ const ChoreView = () => {
>
{
color='neutral'
variant='plain'
fullWidth
+ disabled={chore.isActive === false}
onClick={() => {
navigate(`/chores/${choreId}/history`)
}}
@@ -609,6 +636,7 @@ const ChoreView = () => {
color='neutral'
variant='plain'
fullWidth
+ disabled={chore.isActive === false}
sx={{
// top right of the card:
flexDirection: 'column',
@@ -725,6 +753,7 @@ const ChoreView = () => {
{
if (e.target.checked) {
setNote('')
@@ -764,6 +793,7 @@ const ChoreView = () => {
{
if (e.target.checked) {
setCompletedDate(
@@ -804,164 +834,188 @@ const ChoreView = () => {
/>
)}
-
+ {chore.isActive === false ? (
+ // Archived chore - only show unarchive button
- {chore.status === 3 ? (
- // Pending approval: Show approve/reject for admins/managers/owners, grayed out button for others
- canApproveReject() ? (
+ }
+ >
+ Unarchive
+
+
+ ) : (
+ // Active chore - show all normal actions
+
+
+ {chore.status === 3 ? (
+ // Pending approval: Show approve/reject for admins/managers/owners, grayed out button for others
+ canApproveReject() ? (
+ <>
+ }
+ sx={{
+ flex: 1,
+ }}
+ >
+ Approve
+
+ }
+ sx={{
+ flex: 1,
+ }}
+ >
+ Reject
+
+ >
+ ) : (
+ }
+ >
+ Pending Approval
+
+ )
+ ) : (
+ // Normal completion flow
<>
}
+ onClick={handleTaskCompletion}
+ disabled={
+ isPendingCompletion ||
+ notInCompletionWindow(chore) ||
+ (chore.lastCompletedDate !== null &&
+ chore.frequencyType === 'once')
+ }
+ color={isPendingCompletion ? 'danger' : 'success'}
+ startDecorator={}
sx={{
- flex: 1,
+ flex: 4,
}}
>
- Approve
+ Mark as done
+
}
+ onClick={() => {
+ setConfirmModelConfig({
+ isOpen: true,
+ title: 'Skip Task',
+
+ message: 'Are you sure you want to skip this task?',
+
+ confirmText: 'Skip',
+ cancelText: 'Cancel',
+ onClose: confirmed => {
+ if (confirmed) {
+ handleSkippingTask()
+ }
+ setConfirmModelConfig({})
+ },
+ })
+ }}
+ disabled={
+ chore.lastCompletedDate !== null &&
+ chore.frequencyType === 'once'
+ }
+ startDecorator={}
sx={{
flex: 1,
}}
>
- Reject
+ Skip
>
- ) : (
- }
- >
- Pending Approval
-
- )
+ )}
+
+ {/* Timer Button - Show split button when timer is active, regular button otherwise */}
+ {[ChoreStatus.ACTIVE, ChoreStatus.PAUSED].includes(chore.status) ? (
+ {
+ if (action === 'pause') {
+ handleChorePause()
+ } else if (action === 'resume') {
+ handleChoreStart()
+ }
+ }}
+ onShowDetails={() => navigate(`/chores/${choreId}/timer`)}
+ onResetTimer={handleResetTimer}
+ onClearAllTime={handleClearAllTime}
+ fullWidth
+ />
+ ) : chore.status === ChoreStatus.PENDING_APPROVAL ? (
+ <>>
) : (
- // Normal completion flow
- <>
- }
- sx={{
- flex: 4,
- }}
- >
- Mark as done
-
-
-
- >
+
)}
- {/* Timer Button - Show split button when timer is active, regular button otherwise */}
- {[ChoreStatus.ACTIVE, ChoreStatus.PAUSED].includes(chore.status) ? (
- {
- if (action === 'pause') {
- handleChorePause()
- } else if (action === 'resume') {
- handleChoreStart()
- }
- }}
- onShowDetails={() => navigate(`/chores/${choreId}/timer`)}
- onResetTimer={handleResetTimer}
- onClearAllTime={handleClearAllTime}
- fullWidth
- />
- ) : chore.status === ChoreStatus.PENDING_APPROVAL ? (
- <>>
- ) : (
-
- )}
-
+ )}
{
performers={performers}
viewOnly={false}
showActions={false}
- // onAction={handleChoreAction}
// Multi-select props
isMultiSelectMode={isMultiSelectMode}
isSelected={selectedChores.has(chore.id)}
diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx
index 19701dd..09819e9 100644
--- a/src/views/Chores/MyChores.jsx
+++ b/src/views/Chores/MyChores.jsx
@@ -209,9 +209,16 @@ const MyChores = () => {
}
// Use project-filtered chores for section grouping
- const choresToGroup = selectedProject
- ? filterByProject(chores, selectedProject.id)
- : chores
+ let choresToGroup = chores
+ if (selectedProject) {
+ if (selectedProject.id === 'default') {
+ // Default project: only show tasks without a projectId
+ choresToGroup = chores.filter(chore => !chore.projectId)
+ } else {
+ // Other projects: use the existing filter function
+ choresToGroup = filterByProject(chores, selectedProject.id)
+ }
+ }
const sections = ChoresGrouper(
selectedChoreSection,
@@ -1084,8 +1091,8 @@ const MyChores = () => {
}
const setSelectedProjectWithCache = project => {
- // Handle the case where project might be null (clearing selection)
- const finalProject = project?.id === 'default' || !project ? null : project
+ // Keep the project as-is, including the default project object
+ const finalProject = project || null
setSelectedProject(finalProject)
console.log('final project', finalProject)
@@ -1157,6 +1164,13 @@ const MyChores = () => {
if (!selectedProject) {
return chores
}
+
+ // Special case: Default project shows only tasks without a projectId
+ if (selectedProject.id === 'default') {
+ return chores.filter(chore => !chore.projectId)
+ }
+
+ // Other projects: use the existing filter function
return filterByProject(chores, selectedProject.id)
}, [chores, selectedProject])
@@ -1183,11 +1197,17 @@ const MyChores = () => {
.search(searchTerm.toLowerCase())
.map(result => result.item)
} else {
- result = filteredChores.filter(
- chore =>
- !selectedProject ||
- filterByProject([chore], selectedProject).length > 0,
- )
+ result = filteredChores.filter(chore => {
+ if (!selectedProject) return true
+
+ // Default project: only show tasks without projectId
+ if (selectedProject.id === 'default') {
+ return !chore.projectId
+ }
+
+ // Other projects: use existing filter
+ return filterByProject([chore], selectedProject.id).length > 0
+ })
}
} else {
let choresToFilter = baseChores
diff --git a/src/views/Modals/Inputs/ProjectModal.jsx b/src/views/Modals/Inputs/ProjectModal.jsx
index 8e67def..94ac505 100644
--- a/src/views/Modals/Inputs/ProjectModal.jsx
+++ b/src/views/Modals/Inputs/ProjectModal.jsx
@@ -125,12 +125,32 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
size='md'
unmountDelay={250}
fullWidth={true}
+ title={project ? 'Edit Project' : 'Create New Project'}
+ footer={
+
+
+
+
+ }
>
-
- {project ? 'Edit Project' : 'Create New Project'}
-
-
-
-
setIsIconPickerOpen(false)}
diff --git a/src/views/Projects/ProjectView.jsx b/src/views/Projects/ProjectView.jsx
index 6d9ad52..20e1053 100644
--- a/src/views/Projects/ProjectView.jsx
+++ b/src/views/Projects/ProjectView.jsx
@@ -413,18 +413,7 @@ const ProjectCard = ({
)
})()
) : (
-
- {project.name.charAt(0).toUpperCase()}
-
+ <>>
)}
@@ -545,7 +534,7 @@ const ProjectCard = ({
const ProjectView = () => {
const { data: projects, isProjectsLoading, isError } = useProjects()
const { data: userProfile } = useUserProfile()
- const { data: chores = [] } = useChores(false) // false to exclude archived
+ const { data: chores = { res: [] } } = useChores(false) // false to exclude archived
const [userProjects, setUserProjects] = useState([])
const [modalOpen, setModalOpen] = useState(false)
@@ -602,23 +591,27 @@ const ProjectView = () => {
// Calculate real task counts from chores data
useEffect(() => {
- if (chores && chores.res && userProjects.length > 0) {
+ if (chores && chores.res) {
const choresList = chores.res
const realCounts = {}
+ // First, count tasks for the default project (tasks without a projectId)
+ const defaultProjectCount = choresList.filter(chore => {
+ const choreProjectId = chore.projectId || chore.project_id
+ return (
+ !choreProjectId ||
+ choreProjectId === '' ||
+ choreProjectId === 'default' ||
+ choreProjectId === null
+ )
+ }).length
+ realCounts['default'] = defaultProjectCount
+
+ // Then count tasks for each user project
userProjects.forEach(project => {
- // Count chores for this project
const choreCount = choresList.filter(chore => {
- // Handle default project (projectId is null, undefined, empty string, or 'default')
- if (project.id === 'default') {
- return (
- !chore.projectId ||
- chore.projectId === '' ||
- chore.projectId === 'default'
- )
- }
- // Handle custom projects - exact match with project ID
- return chore.projectId === project.id
+ const choreProjectId = chore.projectId || chore.project_id
+ return choreProjectId === project.id
}).length
realCounts[project.id] = choreCount
diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx
index c726dfd..9361ce7 100644
--- a/src/views/components/AddTaskModal.jsx
+++ b/src/views/components/AddTaskModal.jsx
@@ -1,5 +1,13 @@
import { Add, EditNotifications } from '@mui/icons-material'
-import { Box, Button, Input, Option, Select, Typography } from '@mui/joy'
+import {
+ Avatar,
+ Box,
+ Button,
+ Input,
+ Option,
+ Select,
+ Typography,
+} from '@mui/joy'
import { FormControl } from '@mui/material'
import * as chrono from 'chrono-node'
import moment from 'moment'
@@ -7,8 +15,11 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
import { useCreateChore } from '../../queries/ChoreQueries'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
+import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import { isPlusAccount } from '../../utils/Helpers'
+import { getIconComponent } from '../../utils/ProjectIcons'
import { useLabels } from '../Labels/LabelQueries'
+import { useProjects } from '../Projects/ProjectQueries'
import {
parseAssignees,
parseDueDate,
@@ -47,10 +58,25 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
const { data: circleMembers, isLoading: isCircleMembersLoading } =
useCircleMembers()
+ const { data: projects = [], isLoading: isProjectsLoading } = useProjects()
const createChoreMutation = useCreateChore()
const { data: userProfile } = useUserProfile()
+ // Get initial project from localStorage (current active project)
+ const getInitialProject = () => {
+ const saved = localStorage.getItem('selectedProject')
+ if (saved) {
+ try {
+ const project = JSON.parse(saved)
+ return project?.id || 'default'
+ } catch {
+ return 'default'
+ }
+ }
+ return 'default'
+ }
+
const [taskText, setTaskText] = useState('')
const [taskTitle, setTaskTitle] = useState('')
const [renderedParts, setRenderedParts] = useState([])
@@ -75,6 +101,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
const [hasSubTasks, setHasSubTasks] = useState(false)
const [hasNotifications, setHasNotifications] = useState(false)
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
+ const [projectId, setProjectId] = useState(getInitialProject())
// set showKeyboardShortcuts true as soon as the user hold ctrl or cmd key:
useEffect(() => {
@@ -473,6 +500,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
setHasSubTasks(false)
setLabelsV2([])
setAssignees([])
+ setProjectId(getInitialProject())
}
const createChore = () => {
@@ -516,6 +544,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
frequencyMetadata: {},
notificationMetadata: {},
subTasks: subTasks?.length > 0 ? subTasks : null,
+ projectId: projectId === 'default' ? null : projectId,
}
if (frequency) {
@@ -560,7 +589,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
})
handleCloseModal(false)
}
- if (userLabelsLoading || isCircleMembersLoading) {
+ if (userLabelsLoading || isCircleMembersLoading || isProjectsLoading) {
return <>>
}
@@ -571,6 +600,44 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
size='lg'
fullWidth={true}
title='Create new task'
+ footer={
+
+
+
+
+ }
>
{
)}
+ {projects.length >= 1 && (
+
+ Project
+
+
+ )}
{
)}
-
-
-
-
)
}
diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx
index 8a4fc41..a5d8057 100644
--- a/src/views/components/NavBar.jsx
+++ b/src/views/components/NavBar.jsx
@@ -94,13 +94,12 @@ const links = [
import { SafeArea } from 'capacitor-plugin-safe-area'
import Z_INDEX from '../../constants/zIndex'
-import { useAuth } from '../../hooks/useAuth.jsx'
import { useResource } from '../../queries/ResourceQueries'
+import { apiClient } from '../../utils/apiClient'
const publicPages = ['/landing', '/privacy', '/terms']
const NavBar = () => {
const { data: resource } = useResource()
- const { logout } = useAuth()
const navigate = useNavigate()
const [drawerOpen, setDrawerOpen] = useState(false)
@@ -272,7 +271,9 @@ const NavBar = () => {
Upgrade to Plus
*/}
{
+ apiClient.handleLogout()
+ }}
sx={{
py: 1.2,
}}
diff --git a/src/views/components/ProjectSelector.jsx b/src/views/components/ProjectSelector.jsx
index 266d5b2..89c4754 100644
--- a/src/views/components/ProjectSelector.jsx
+++ b/src/views/components/ProjectSelector.jsx
@@ -1,4 +1,4 @@
-import { Add, Check, FolderOpen } from '@mui/icons-material'
+import { Add, Check, FolderOpen, Settings } from '@mui/icons-material'
import {
Avatar,
Box,
@@ -11,6 +11,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useRef, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
import LABEL_COLORS, {
getTextColorFromBackgroundColor,
@@ -25,9 +26,11 @@ const ProjectSelector = ({
showKeyboardShortcuts = false,
}) => {
const { data: projects = [], isLoading } = useProjects()
+ const navigate = useNavigate()
const [anchorEl, setAnchorEl] = useState(null)
const [selectedIndex, setSelectedIndex] = useState(0)
+ const [isKeyboardNavigating, setIsKeyboardNavigating] = useState(false)
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false)
const menuRef = useRef(null)
const buttonRef = useRef(null)
@@ -56,6 +59,7 @@ const ProjectSelector = ({
const handleMenuOpen = event => {
setAnchorEl(event.currentTarget)
+ setIsKeyboardNavigating(false)
}
const handleMenuClose = () => {
@@ -76,6 +80,11 @@ const ProjectSelector = ({
handleProjectSelect(project)
}
+ const handleManageProjects = () => {
+ navigate('/projects')
+ handleMenuClose()
+ }
+
useEffect(() => {
const handleMenuOutsideClick = event => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
@@ -100,6 +109,7 @@ const ProjectSelector = ({
if (!anchorEl) {
setAnchorEl(buttonRef.current)
setSelectedIndex(0)
+ setIsKeyboardNavigating(true)
} else {
handleMenuClose()
}
@@ -112,20 +122,33 @@ const ProjectSelector = ({
switch (event.key) {
case 'ArrowDown':
event.preventDefault()
+ setIsKeyboardNavigating(true)
setSelectedIndex(prev =>
- prev < defaultProjects.length ? prev + 1 : prev,
+ prev < defaultProjects.length + 2 ? prev + 1 : prev,
)
break
case 'ArrowUp':
event.preventDefault()
+ setIsKeyboardNavigating(true)
setSelectedIndex(prev => (prev > 0 ? prev - 1 : prev))
break
case 'Enter':
event.preventDefault()
- if (selectedIndex < defaultProjects.length) {
- handleProjectSelect(defaultProjects[selectedIndex])
- } else {
+ if (selectedIndex === 0) {
+ // Hardcoded Default Project
+ handleProjectSelect({
+ id: 'default',
+ name: 'Default Project',
+ color: LABEL_COLORS[0].value,
+ icon: 'FolderOpen',
+ })
+ } else if (selectedIndex <= defaultProjects.length) {
+ // Projects from the array (offset by 1)
+ handleProjectSelect(defaultProjects[selectedIndex - 1])
+ } else if (selectedIndex === defaultProjects.length + 1) {
handleAddProjectClick()
+ } else {
+ handleManageProjects()
}
break
case 'Escape':
@@ -139,7 +162,7 @@ const ProjectSelector = ({
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
- }, [anchorEl, selectedIndex, defaultProjects])
+ }, [anchorEl, selectedIndex, defaultProjects, isKeyboardNavigating])
// Reset selected index when menu opens
useEffect(() => {
@@ -233,7 +256,6 @@ const ProjectSelector = ({
disabled
sx={{
borderRadius: 'var(--joy-radius-sm)',
- mb: 1,
cursor: 'default',
opacity: 1,
}}
@@ -243,13 +265,7 @@ const ProjectSelector = ({
- Select Project
-
-
- Choose or create a project workspace
+ Projects
@@ -266,12 +282,13 @@ const ProjectSelector = ({
icon: 'FolderOpen',
})
}
+ onMouseEnter={() => setIsKeyboardNavigating(false)}
sx={{
borderRadius: 'var(--joy-radius-sm)',
backgroundColor:
effectiveSelectedProject === 'Default Project'
? 'var(--joy-palette-primary-softBg)'
- : selectedIndex === 0 && anchorEl
+ : selectedIndex === 0 && anchorEl && isKeyboardNavigating
? 'var(--joy-palette-neutral-softHoverBg)'
: 'transparent',
'&:hover': {
@@ -345,12 +362,13 @@ const ProjectSelector = ({