Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 204 additions & 46 deletions view/components/layout/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
'use client';

import * as React from 'react';
import { Folder, Home, Package, SettingsIcon, Container } from 'lucide-react';
import {
Folder,
Home,
Package,
SettingsIcon,
Container,
LucideProps
} from 'lucide-react';
import { NavMain } from '@/components/layout/nav-main';
import { NavUser } from '@/components/layout/nav-user';
import { TeamSwitcher } from '@/components/ui/team-switcher';
Expand All @@ -19,7 +26,60 @@ import { setActiveOrganization } from '@/redux/features/users/userSlice';
import { useTranslation } from '@/hooks/use-translation';
import { useRBAC } from '@/lib/rbac';

const data = {
// Add proper TypeScript interfaces

// 1. Defined specific types for User and Organization
interface User {
id: string;
name?: string;
email?: string;
// Add other user properties as needed
}

interface Organization {
id: string;
name?: string;
// Add other organization properties as needed
}

// 2. Updated AppState to use the specific types instead of 'any'
interface AppState {
auth: {
user: User | null;
};
user: {
organizations: { organization: Organization }[];
activeOrganization: Organization | null;
};
}

// 3. Created a strict type for resource strings for type safety
type Resource =
| 'dashboard'
| 'deploy'
| 'container'
| 'file-manager'
| 'settings'
| 'notification'
| 'organization'
| 'domain';

interface NavSubItem {
title: string;
url: string;
resource: Resource;
}

// 4. Updated NavItem to use the Resource type and a more specific icon type
interface NavItem {
title: string;
url: string;
icon: React.ComponentType<LucideProps>;
resource: Resource;
items?: NavSubItem[];
}

const data: { navMain: NavItem[] } = {
navMain: [
{
title: 'navigation.dashboard',
Expand Down Expand Up @@ -76,63 +136,62 @@ const data = {
]
};

export function AppSidebar({
toggleAddTeamModal,
...props
}: React.ComponentProps<typeof Sidebar> & { toggleAddTeamModal?: () => void }) {
// 5. Removed the unused 'toggleAddTeamModal' prop
export function AppSidebar(props: React.ComponentProps<typeof Sidebar>) {
const { t } = useTranslation();
const user = useAppSelector((state) => state.auth.user);
const { isLoading, refetch } = useGetUserOrganizationsQuery();
const organizations = useAppSelector((state) => state.user.organizations);
const user = useAppSelector((state: AppState) => state.auth.user);
const { refetch } = useGetUserOrganizationsQuery();
const organizations = useAppSelector((state: AppState) => state.user.organizations);
const { activeNav, setActiveNav } = useNavigationState();
const activeOrg = useAppSelector((state) => state.user.activeOrganization);
const activeOrg = useAppSelector((state: AppState) => state.user.activeOrganization);
const dispatch = useAppDispatch();
const { canAccessResource } = useRBAC();

const hasAnyPermission = React.useMemo(() => {
const allowedResources = ['dashboard', 'settings'];
const allowedResources: Resource[] = ['dashboard', 'settings'];

return (resource: string) => {
return (resource: Resource) => {
if (!user || !activeOrg) return false;

if (allowedResources.includes(resource)) {
return true;
}

return (
canAccessResource(resource as any, 'read') ||
canAccessResource(resource as any, 'create') ||
canAccessResource(resource as any, 'update') ||
canAccessResource(resource as any, 'delete')
canAccessResource(resource, 'read') ||
canAccessResource(resource, 'create') ||
canAccessResource(resource, 'update') ||
canAccessResource(resource, 'delete')
);
};
}, [user, activeOrg, canAccessResource]);

const filteredNavItems = React.useMemo(
() =>
data.navMain
.filter((item) => {
if (!item.resource) return false;

if (item.items) {
const filteredSubItems = item.items.filter(
(subItem) => subItem.resource && hasAnyPermission(subItem.resource)
);
return filteredSubItems.length > 0;
}

return hasAnyPermission(item.resource);
})
.map((item) => ({
...item,
title: t(item.title),
items: item.items?.map((subItem) => ({
// 6. Corrected and optimized the logic for filtering and mapping navigation items
const filteredNavItems = React.useMemo(() => {
return data.navMain.reduce<NavItem[]>((acc, item) => {
let visibleSubItems: NavSubItem[] | undefined;

if (item.items) {
visibleSubItems = item.items
.filter(subItem => hasAnyPermission(subItem.resource))
.map(subItem => ({
...subItem,
title: t(subItem.title)
}))
})),
[data.navMain, hasAnyPermission, t]
);
}));
}

// An item is visible if it has direct permission OR it has visible sub-items
if (hasAnyPermission(item.resource) || (visibleSubItems && visibleSubItems.length > 0)) {
acc.push({
...item,
title: t(item.title),
items: visibleSubItems
});
}

return acc;
}, []);
}, [hasAnyPermission, t]);

React.useEffect(() => {
if (organizations && organizations.length > 0 && !activeOrg) {
Expand All @@ -146,6 +205,77 @@ export function AppSidebar({
}
}, [activeOrg?.id, refetch]);

const handleSponsorClick = React.useCallback((): void => {
window.open('https://github.com/sponsors/raghavyuva', '_blank');
}, []);

const handleHelpClick = React.useCallback((): void => {
window.open('https://docs.nixopus.com', '_blank');
}, []);

const handleReportIssueClick = React.useCallback((): void => {
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
const browser = userAgent.includes('Chrome')
? 'Chrome'
: userAgent.includes('Firefox')
? 'Firefox'
: userAgent.includes('Safari')
? 'Safari'
: userAgent.includes('Edge')
? 'Edge'
: 'Unknown';

const os = userAgent.includes('Windows')
? 'Windows'
: userAgent.includes('Mac')
? 'macOS'
: userAgent.includes('Linux')
? 'Linux'
: userAgent.includes('Android')
? 'Android'
: userAgent.includes('iPhone') || userAgent.includes('iPad')
? 'iOS'
: 'Unknown';

const screenResolution =
typeof screen !== 'undefined' ? `${screen.width}x${screen.height}` : 'Unknown';
const language = typeof navigator !== 'undefined' ? navigator.language : 'Unknown';
const timezone =
typeof Intl !== 'undefined' && Intl.DateTimeFormat
? Intl.DateTimeFormat().resolvedOptions().timeZone
: 'Unknown';

const issueBody = `**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Additional context**
- Browser: ${browser}
- Operating System: ${os}
- Screen Resolution: ${screenResolution}
- Language: ${language}
- Timezone: ${timezone}
- User Agent: ${userAgent}

Add any other context about the problem here.`;

const encodedBody = encodeURIComponent(issueBody);
const url = `https://github.com/raghavyuva/nixopus/issues/new?template=bug_report.md&body=${encodedBody}`;
window.open(url, '_blank');
}, []);

if (!user || !activeOrg) {
return null;
}
Expand All @@ -156,21 +286,49 @@ export function AppSidebar({
<TeamSwitcher refetch={refetch} />
</SidebarHeader>
<SidebarContent>
{/* 7. Simplified NavMain props, as filtering is now handled correctly in useMemo */}
<NavMain
items={filteredNavItems.map((item) => ({
...item,
isActive: item.url === activeNav,
items: item.items?.filter(
(subItem) => subItem.resource && hasAnyPermission(subItem.resource)
)
isActive: item.url === activeNav
}))}
onItemClick={(url) => setActiveNav(url)}
onItemClick={(url: string) => setActiveNav(url)}
/>
</SidebarContent>
<SidebarFooter>
<NavUser user={user} />
<div className="w-full">
<NavUser user={user} />
</div>

<div className="w-full px-3 py-3">
<div className="flex flex-col gap-2">
<button
type="button"
onClick={handleSponsorClick}
className="w-full py-2 rounded-lg border"
>
{t('user.menu.sponsor') || 'Sponsor'}
</button>

<button
type="button"
onClick={handleHelpClick}
className="w-full py-2 rounded-lg border"
>
{t('user.menu.help') || 'Help'}
</button>

<button
type="button"
onClick={handleReportIssueClick}
className="w-full py-2 rounded-lg border"
>
{t('user.menu.reportIssue') || 'Report Issue'}
</button>
Comment on lines +310 to +327
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the translation fallback.

t() always returns a truthy string (the translated value or the key itself), so t('user.menu.sponsor') || 'Sponsor' never hits the fallback and you end up rendering the raw i18n key while translations load. Compare against the key explicitly and reuse the helper for all three buttons:

+  const translateOrFallback = React.useCallback(
+    (key: string, fallback: string) => {
+      const value = t(key);
+      return value === key ? fallback : value;
+    },
+    [t]
+  );
...
-              {t('user.menu.sponsor') || 'Sponsor'}
+              {translateOrFallback('user.menu.sponsor', 'Sponsor')}
...
-              {t('user.menu.help') || 'Help'}
+              {translateOrFallback('user.menu.help', 'Help')}
...
-              {t('user.menu.reportIssue') || 'Report Issue'}
+              {translateOrFallback('user.menu.reportIssue', 'Report Issue')}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{t('user.menu.sponsor') || 'Sponsor'}
</button>
<button
type="button"
onClick={handleHelpClick}
className="w-full py-2 rounded-lg border"
>
{t('user.menu.help') || 'Help'}
</button>
<button
type="button"
onClick={handleReportIssueClick}
className="w-full py-2 rounded-lg border"
>
{t('user.menu.reportIssue') || 'Report Issue'}
</button>
// (Some lines earlier in your component…)
const { t } = useTranslation();
const translateOrFallback = React.useCallback(
(key: string, fallback: string) => {
const value = t(key);
return value === key ? fallback : value;
},
[t]
);
// …then in your JSX render:
<button
type="button"
onClick={handleSponsorClick}
className="w-full py-2 rounded-lg border"
>
{translateOrFallback('user.menu.sponsor', 'Sponsor')}
</button>
<button
type="button"
onClick={handleHelpClick}
className="w-full py-2 rounded-lg border"
>
{translateOrFallback('user.menu.help', 'Help')}
</button>
<button
type="button"
onClick={handleReportIssueClick}
className="w-full py-2 rounded-lg border"
>
{translateOrFallback('user.menu.reportIssue', 'Report Issue')}
</button>
🤖 Prompt for AI Agents
In view/components/layout/app-sidebar.tsx around lines 310 to 327, the code uses
t('user.menu.sponsor') || 'Sponsor' (and similar for Help and Report Issue) but
t() always returns a string so the fallback never runs; update the JSX to use a
reuseable check that compares the returned value to the key (e.g. if t(key) ===
key then use the human fallback) and apply that helper for all three buttons
(Sponsor, Help, Report Issue) so the readable fallback text is rendered while
translations are loading.

</div>
</div>
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
}
}
Loading