Skip to content

Commit 8192b1a

Browse files
committed
feat: add tooltips for delegation views and fix pagination testing
1 parent 5db0b46 commit 8192b1a

2 files changed

Lines changed: 172 additions & 38 deletions

File tree

src/apps/ums.web-app/src/application/identity/hooks/use-delegation-dashboard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export function useDelegationDashboard(): DelegationDashboardState & DelegationD
8080
const [page, setPage] = useState(1);
8181
const [delegationViewType, setDelegationViewType] = useState<DelegationViewType>('received');
8282

83-
const pageSize = 10;
83+
const pageSize = 2;
8484

8585
const receivedQuery = useGetDelegationsByDelegatedAdmin(CURRENT_USER_ID, CURRENT_TENANT_ID);
8686
const grantedQuery = useGetDelegationsByDelegatingAdmin(CURRENT_USER_ID, CURRENT_TENANT_ID);
Lines changed: 171 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,89 @@
11
import React, { useCallback } from 'react';
2-
import { ArrowRight, Shield } from 'lucide-react';
2+
import { Shield, ArrowRight, Info } from 'lucide-react';
33
import type { Delegation } from '@domain/identity/models/delegation.model';
44
import { StatusBadge } from '@shared/components/StatusBadge';
55
import { CodeBadge } from '@shared/components/CodeBadge';
66
import { EntityRow } from '@shared/components/EntityRow';
77
import { useI18n } from '@app/i18n/use-i18n';
88
import { useStatusLabel } from '@app/hooks/use-status-label';
9-
9+
import {
10+
M3DataView,
11+
SortOption,
12+
FilterOption,
13+
QueryCriteriaOption,
14+
} from '@shared/components/M3DataView';
15+
import { ApiErrorBanner } from '@shared/components/ApiErrorBanner';
16+
import { DelegationViewType } from '@app/identity/hooks/use-delegation-dashboard';
17+
import { Tooltip } from '@shared/components/Tooltip';
1018

1119
interface DelegationListPanelProps {
1220
delegations: Delegation[];
1321
selectedId: string;
1422
isLoading: boolean;
1523
error: Error | null;
16-
title: string;
17-
emptyLabel: string;
24+
viewMode: 'list' | 'thumbnail';
25+
onViewModeChange: (mode: 'list' | 'thumbnail') => void;
26+
searchCriteria: string;
27+
onSearchCriteriaChange: (criteria: string) => void;
28+
searchValue: string;
29+
onSearchValueChange: (value: string) => void;
30+
onSearchSubmit: (event: React.FormEvent) => void;
31+
onRegisterNew: () => void;
32+
sortBy: string;
33+
onSortByChange: (value: string) => void;
34+
sortOrder: 'asc' | 'desc';
35+
onSortOrderToggle: () => void;
36+
activeFilter: string;
37+
onFilterChange: (value: string) => void;
38+
page: number;
39+
pageSize: number;
40+
totalItems: number;
41+
totalPages: number;
42+
startIndex: number;
43+
appliedTerm: string;
44+
onPageChange: (page: number) => void;
45+
onResetQuery: () => void;
1846
onSelectDelegation: (delegationId: string) => void;
19-
onRegisterNew?: () => void;
47+
criteriaOptions: QueryCriteriaOption[];
48+
filterOptions: FilterOption[];
49+
sortOptions: SortOption[];
50+
delegationViewType: DelegationViewType;
51+
onDelegationViewTypeChange: (type: DelegationViewType) => void;
2052
}
2153

22-
23-
2454
export const DelegationListPanel: React.FC<DelegationListPanelProps> = ({
2555
delegations,
2656
selectedId,
2757
isLoading,
2858
error,
29-
title,
30-
emptyLabel,
31-
onSelectDelegation,
59+
viewMode,
60+
onViewModeChange,
61+
searchCriteria,
62+
onSearchCriteriaChange,
63+
searchValue,
64+
onSearchValueChange,
65+
onSearchSubmit,
3266
onRegisterNew,
67+
sortBy,
68+
onSortByChange,
69+
sortOrder,
70+
onSortOrderToggle,
71+
activeFilter,
72+
onFilterChange,
73+
page,
74+
pageSize,
75+
totalItems,
76+
totalPages,
77+
startIndex,
78+
appliedTerm,
79+
onPageChange,
80+
onResetQuery,
81+
onSelectDelegation,
82+
criteriaOptions,
83+
filterOptions,
84+
sortOptions,
85+
delegationViewType,
86+
onDelegationViewTypeChange,
3387
}) => {
3488
const t = useI18n();
3589
const getStatusLabel = useStatusLabel();
@@ -67,40 +121,120 @@ export const DelegationListPanel: React.FC<DelegationListPanelProps> = ({
67121
);
68122
}, [selectedId, onSelectDelegation, getStatusLabel]);
69123

124+
const renderDelegationCard = useCallback((delegation: Delegation) => {
125+
const isSelected = delegation.delegationId === selectedId;
126+
return (
127+
<div
128+
key={delegation.delegationId}
129+
onClick={() => onSelectDelegation(delegation.delegationId)}
130+
className={`p-4 rounded-xl border cursor-pointer transition-colors ${
131+
isSelected
132+
? 'border-m3-primary bg-m3-primary/5'
133+
: 'border-m3-outline/20 bg-m3-surface hover:bg-m3-surface-container'
134+
}`}
135+
>
136+
<div className="flex items-start justify-between mb-3">
137+
<div className="flex items-center gap-2">
138+
<Shield className={`w-5 h-5 ${isSelected ? 'text-m3-primary' : 'text-m3-secondary'}`} />
139+
<span className="text-sm font-medium">{delegation.delegationId.substring(0, 8)}...</span>
140+
</div>
141+
<StatusBadge status={delegation.status} label={getStatusLabel(delegation.status)} />
142+
</div>
143+
<div className="flex items-center gap-2 mt-2">
144+
<span className="text-xs text-m3-on-surface-variant">Scope:</span>
145+
<CodeBadge code={delegation.scopeType} />
146+
</div>
147+
</div>
148+
);
149+
}, [selectedId, onSelectDelegation, getStatusLabel]);
150+
151+
const footerTelemetry = (
152+
<div className="flex items-center gap-3">
153+
<div className="flex items-center gap-1.5">
154+
<span className="h-2 w-2 rounded-full bg-m3-primary animate-pulse" />
155+
<span className="text-xs font-medium text-m3-secondary/80">
156+
{t.showing ?? 'Showing'} {totalItems === 0 ? 0 : startIndex + 1}-{Math.min(startIndex + pageSize, totalItems)} {t.of ?? 'of'} {totalItems} Delegations
157+
</span>
158+
</div>
159+
{appliedTerm.trim() && (
160+
<button onClick={onResetQuery} className="text-xs font-medium text-rose-500 hover:underline flex items-center gap-1">
161+
<Info className="w-3 h-3" /> {t.clearFilter ?? 'Clear Filters'}
162+
</button>
163+
)}
164+
</div>
165+
);
166+
70167
return (
71168
<div className="flex flex-col h-full">
72-
<div className="flex items-center justify-between px-4 py-3 border-b border-m3-outline/10">
73-
<h2 className="text-sm font-semibold text-m3-on-surface">{title}</h2>
74-
{onRegisterNew && (
75-
<button
76-
onClick={onRegisterNew}
77-
className="text-xs text-m3-primary hover:underline"
78-
type="button"
169+
<div className="flex border-b border-m3-outline/15 px-4 pt-2 mb-2 bg-m3-surface-container/10">
170+
<Tooltip content="Delegaciones que has recibido (donde tú eres el administrador delegado).">
171+
<button
172+
onClick={() => onDelegationViewTypeChange('received')}
173+
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${delegationViewType === 'received' ? 'border-m3-primary text-m3-primary' : 'border-transparent text-m3-on-surface-variant hover:text-m3-on-surface'}`}
79174
>
80-
{t.registerNew ?? 'New'}
175+
Received Delegations
81176
</button>
82-
)}
177+
</Tooltip>
178+
<Tooltip content="Delegaciones que has otorgado a otros (donde tú eres el administrador delegador).">
179+
<button
180+
onClick={() => onDelegationViewTypeChange('granted')}
181+
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${delegationViewType === 'granted' ? 'border-m3-primary text-m3-primary' : 'border-transparent text-m3-on-surface-variant hover:text-m3-on-surface'}`}
182+
>
183+
Granted Delegations
184+
</button>
185+
</Tooltip>
83186
</div>
84-
85-
<div className="flex-1 overflow-y-auto px-2 py-2">
86-
{isLoading && (
87-
<p className="text-xs text-m3-secondary px-2 py-4 text-center">
88-
{t.loadingAccounts ?? 'Loading...'}
89-
</p>
90-
)}
91-
92-
{!isLoading && error && <ErrorBanner error={error} />}
93-
94-
{!isLoading && !error && delegations.length === 0 && (
95-
<p className="text-xs text-m3-secondary px-2 py-4 text-center">{emptyLabel}</p>
187+
<M3DataView
188+
title={'Delegation Management'}
189+
subtitle={delegationViewType === 'received' ? 'Delegations granted to you' : 'Delegations you have granted to others'}
190+
searchPlaceholder={t.searchPlaceholder ?? 'Search...'}
191+
searchCriteria={criteriaOptions}
192+
activeCriteria={searchCriteria}
193+
onCriteriaChange={onSearchCriteriaChange}
194+
searchValue={searchValue}
195+
onSearchValueChange={onSearchValueChange}
196+
onSearchSubmit={onSearchSubmit}
197+
onRegisterNew={onRegisterNew}
198+
registerLabel={t.newBtn ?? 'New'}
199+
viewMode={viewMode}
200+
onViewModeChange={onViewModeChange}
201+
sortOptions={sortOptions}
202+
sortBy={sortBy}
203+
onSortByChange={onSortByChange}
204+
sortOrder={sortOrder}
205+
onSortOrderToggle={onSortOrderToggle}
206+
filterOptions={filterOptions}
207+
activeFilter={activeFilter}
208+
onFilterChange={onFilterChange}
209+
isLoading={isLoading}
210+
isEmpty={totalItems === 0}
211+
emptyLabel={t.noRecords ?? 'No records found'}
212+
emptyTitle={t.dataViewEmptyTitle ?? 'No Results'}
213+
loadingLabel={t.dataViewLoading ?? 'Loading...'}
214+
criteriaLabel={t.dataViewCriteriaLabel ?? 'Search by'}
215+
searchTermLabel={t.dataViewSearchTermLabel ?? 'Search term'}
216+
searchButtonLabel={t.dataViewSearchBtn ?? 'Search'}
217+
renderList={() => (
218+
<>
219+
{error && <ApiErrorBanner error={error} />}
220+
<div className="overflow-x-auto border border-m3-outline/25 rounded-xl bg-m3-surface-container/20">
221+
<div className="divide-y divide-m3-outline/10 text-sm">
222+
{delegations.map(renderDelegationRow)}
223+
</div>
224+
</div>
225+
</>
96226
)}
97-
98-
{!isLoading && delegations.length > 0 && (
99-
<div className="divide-y divide-m3-outline/10">
100-
{delegations.map(renderDelegationRow)}
101-
</div>
227+
renderThumbnail={() => (
228+
<>
229+
{error && <ApiErrorBanner error={error} />}
230+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
231+
{delegations.map(renderDelegationCard)}
232+
</div>
233+
</>
102234
)}
103-
</div>
235+
pagination={{ page, pageSize, totalItems, totalPages, onPageChange }}
236+
telemetryInfo={footerTelemetry}
237+
/>
104238
</div>
105239
);
106240
};

0 commit comments

Comments
 (0)