Skip to content
Merged
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
17 changes: 16 additions & 1 deletion src/app/commitments/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { notFound, useRouter } from 'next/navigation';
import CommitmentDetailHeader from '@/components/Commitmentdetailheader';
import CommitmentHealthMetrics from '@/components/dashboard/CommitmentHealthMetrics';
Expand All @@ -22,6 +22,8 @@ import { useToast } from '@/components/toast/ToastProvider';
import { getAppExplorerNetwork } from './explorerNetwork';
import { useRecentlyViewed, RECENTLY_VIEWED_COMMITMENTS_KEY } from '@/hooks/useRecentlyViewed';
import { RecentlyViewedCommitmentsRail } from '@/components/RecentlyViewedCommitmentsRail';
import { useRegisterCommands } from '@/components/CommandPalette';
import { buildCommitmentScopedCommands } from '@/components/CommandPalette/scopedActions';

// Mock Commitments
const MOCK_COMMITMENTS: Record<
Expand Down Expand Up @@ -244,6 +246,19 @@ export default function CommitmentDetailPage({ params }: { params: { id: string
showSuccess({ title: 'Coming Soon', description: 'Settlement is not yet available.' });
}, [showSuccess]);

const scopedCommands = useMemo(
() =>
buildCommitmentScopedCommands({
commitmentId: commitment.id,
canSettle: commitmentStatusOverride !== 'Disputed',
canEarlyExit: commitment.canEarlyExit,
onSettle: handleSettle,
onEarlyExit: handleEarlyExit,
}),
[commitment.id, commitment.canEarlyExit, commitmentStatusOverride, handleSettle, handleEarlyExit],
);
useRegisterCommands(scopedCommands);

return (
<CommitmentStatusProvider commitmentId={commitment.id}>
<main
Expand Down
7 changes: 5 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from 'next';
import './globals.css';
import ScrollToTopButton from '@/components/landing-page/ui/ScrollToTop';
import { SITE_URL } from '@/lib/site';
import { CommandPalette, CommandPaletteProvider } from '@/components/CommandPalette';

export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
Expand Down Expand Up @@ -93,9 +94,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<MotionProvider>
<ToastProvider>
<NetworkMismatchBanner />
<AppShellConnectionStatus>{children}</AppShellConnectionStatus>
<CommandPaletteProvider>
<AppShellConnectionStatus>{children}</AppShellConnectionStatus>
<CommandPalette />
</CommandPaletteProvider>
<ScrollToTopButton />
<CommandPaletteProvider />
</ToastProvider>
</MotionProvider>
</WalletProvider>
Expand Down
40 changes: 40 additions & 0 deletions src/components/CommandPalette/scopedActions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from 'vitest';
import { buildCommitmentScopedCommands } from './scopedActions';

describe('buildCommitmentScopedCommands', () => {
it('creates scoped actions with stable ids and handlers', () => {
const onSettle = vi.fn();
const onEarlyExit = vi.fn();
const commands = buildCommitmentScopedCommands({
commitmentId: 'c-1',
canSettle: true,
canEarlyExit: true,
onSettle,
onEarlyExit,
});

expect(commands.map((command) => command.id)).toEqual([
'commitment:c-1:settle',
'commitment:c-1:early-exit',
'commitment:c-1:list-for-sale',
]);
commands[0].run();
commands[1].run();
expect(onSettle).toHaveBeenCalledOnce();
expect(onEarlyExit).toHaveBeenCalledOnce();
});

it('keeps unavailable actions visible but disabled with reasons', () => {
const [settle, earlyExit, listForSale] = buildCommitmentScopedCommands({
commitmentId: 'c-2',
canSettle: false,
canEarlyExit: false,
onSettle: vi.fn(),
onEarlyExit: vi.fn(),
});

expect(settle.disabledReason).toBeTruthy();
expect(earlyExit.disabledReason).toBeTruthy();
expect(listForSale.disabledReason).toBeTruthy();
});
});
48 changes: 48 additions & 0 deletions src/components/CommandPalette/scopedActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { CommandItem } from './types';

export interface CommitmentActionHandlers {
onSettle: () => void;
onEarlyExit: () => void;
}

export interface CommitmentActionState extends CommitmentActionHandlers {
commitmentId: string;
canSettle: boolean;
canEarlyExit: boolean;
}

/** Builds the contextual commands shown while viewing one commitment. */
export function buildCommitmentScopedCommands({
commitmentId,
canSettle,
canEarlyExit,
onSettle,
onEarlyExit,
}: CommitmentActionState): CommandItem[] {
return [
{
id: `commitment:${commitmentId}:settle`,
label: 'Settle commitment',
group: 'Commitment actions',
disabled: !canSettle,
disabledReason: 'Settlement is not available for this commitment.',
run: onSettle,
},
{
id: `commitment:${commitmentId}:early-exit`,
label: 'Early exit commitment',
group: 'Commitment actions',
disabled: !canEarlyExit,
disabledReason: 'Early exit is only available before maturity.',
run: onEarlyExit,
},
{
id: `commitment:${commitmentId}:list-for-sale`,
label: 'List commitment for sale',
group: 'Commitment actions',
disabled: true,
disabledReason: 'Listing commitments for sale is not available yet.',
run: () => undefined,
},
];
}
Loading