tududi/frontend/components/Sidebar/SidebarNav.tsx
Chris 57a6e558f3
fix: use CALDAV_ENABLED for calendar feature flag (#1070)
* fix: add FF_ENABLE_CALDAV feature flag for CalDAV functionality

Introduces a new dedicated feature flag for CalDAV sync that checks
both FF_ENABLE_CALDAV and CALDAV_ENABLED environment variables. This
allows the CalDAV tab to appear in profile settings when users set
CALDAV_ENABLED=true as documented.

The existing FF_ENABLE_CALENDAR remains unchanged as it's for a
separate (hidden) calendar feature.

Changes:
- Added 'caldav' feature flag to backend service (checks FF_ENABLE_CALDAV
  or CALDAV_ENABLED)
- Updated frontend FeatureFlags interface to include 'caldav'
- Changed CalDAV tab to use 'caldav' feature flag instead of 'calendar'
- Added FF_ENABLE_CALDAV to .env.example, .env.test, Dockerfile, and CI

Fixes #1048

* fix: add caldav property to all FeatureFlags initializations

Fixes TypeScript errors where FeatureFlags objects were missing the
new caldav property in:
- frontend/utils/featureFlags.ts (defaultFlags and error return)
- frontend/components/Navbar.tsx
- frontend/components/Sidebar.tsx
- frontend/components/Sidebar/SidebarNav.tsx
2026-04-25 18:21:53 +03:00

180 lines
7 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { Location } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
CalendarDaysIcon,
InboxIcon,
ListBulletIcon,
ClockIcon,
CalendarIcon,
} from '@heroicons/react/24/solid';
import { PlusCircleIcon } from '@heroicons/react/24/outline';
import { useStore } from '../../store/useStore';
import { loadInboxItemsToStore } from '../../utils/inboxService';
import { getFeatureFlags, FeatureFlags } from '../../utils/featureFlags';
interface SidebarNavProps {
handleNavClick: (path: string, title: string, icon: JSX.Element) => void;
location: Location;
isDarkMode: boolean;
openTaskModal: () => void;
}
const SidebarNav: React.FC<SidebarNavProps> = ({
handleNavClick,
location,
openTaskModal,
}) => {
const { t } = useTranslation();
const store = useStore();
const [featureFlags, setFeatureFlags] = useState<FeatureFlags>({
backups: false,
calendar: false,
caldav: false,
habits: false,
mcp: false,
});
const inboxItemsCount = store.inboxStore.pagination.total;
useEffect(() => {
loadInboxItemsToStore(false).catch(console.error);
const fetchFlags = async () => {
const flags = await getFeatureFlags();
setFeatureFlags(flags);
};
fetchFlags();
}, []);
const allNavLinks = [
{
path: '/inbox',
title: t('sidebar.inbox', 'Inbox'),
icon: <InboxIcon className="h-5 w-5" />,
},
{
path: '/today',
title: t('sidebar.today', 'Today'),
icon: <CalendarDaysIcon className="h-5 w-5" />,
query: 'type=today',
},
{
path: '/upcoming?status=active',
title: t('sidebar.upcoming', 'Upcoming'),
icon: <ClockIcon className="h-5 w-5" />,
},
{
path: '/calendar',
title: t('sidebar.calendar', 'Calendar'),
icon: <CalendarIcon className="h-5 w-5" />,
featureFlag: 'calendar',
},
{
path: '/tasks?status=active',
title: t('sidebar.allTasks', 'All Tasks'),
icon: <ListBulletIcon className="h-5 w-5" />,
query: 'status=active',
},
];
const navLinks = allNavLinks.filter((link) => {
if (link.featureFlag) {
return featureFlags[link.featureFlag as keyof FeatureFlags];
}
return true;
});
const isActive = (path: string, query?: string) => {
if (path === '/inbox' || path === '/today' || path === '/calendar') {
const isPathMatch = location.pathname === path;
return isPathMatch
? 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white'
: 'text-gray-700 dark:text-gray-300';
}
if (path.startsWith('/upcoming')) {
const isPathMatch = location.pathname === '/upcoming';
return isPathMatch
? 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white'
: 'text-gray-700 dark:text-gray-300';
}
const isPathMatch = location.pathname === '/tasks';
const isQueryMatch = query
? location.search.includes(query)
: location.search === '';
return isPathMatch && isQueryMatch
? 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white'
: 'text-gray-700 dark:text-gray-300';
};
return (
<ul className="flex flex-col space-y-1">
{navLinks.map((link) => (
<React.Fragment key={link.path}>
<li>
<button
onClick={() =>
handleNavClick(link.path, link.title, link.icon)
}
data-testid={`sidebar-nav-${link.path.replace(/^\//, '').replace(/\?.*$/, '')}`}
className={`w-full text-left px-4 py-1 flex items-center justify-between rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 ${isActive(
link.path,
link.query
)}`}
>
<div className="flex items-center">
{link.icon}
<span className="ml-2">{link.title}</span>
</div>
<div className="flex items-center gap-2">
{link.path === '/inbox' &&
inboxItemsCount > 0 && (
<span className="text-sm font-bold text-blue-500 dark:text-blue-400">
{inboxItemsCount > 99
? '99+'
: inboxItemsCount}
</span>
)}
{link.path === '/tasks?status=active' && (
<div
role="button"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
openTaskModal();
}}
onKeyDown={(e) => {
if (
e.key === 'Enter' ||
e.key === ' '
) {
e.stopPropagation();
e.preventDefault();
openTaskModal();
}
}}
className="text-gray-700 dark:text-gray-300 hover:text-black dark:hover:text-white focus:outline-none cursor-pointer"
aria-label={t(
'sidebar.addTaskAriaLabel',
'Add Task'
)}
title={t(
'sidebar.addTaskTitle',
'Add Task'
)}
>
<PlusCircleIcon className="h-5 w-5" />
</div>
)}
</div>
</button>
</li>
</React.Fragment>
))}
</ul>
);
};
export default SidebarNav;