Skip to content

Commit 7a61ede

Browse files
authored
[FEAT] Tabs rework (#1281)
* feat: tabs ui-design rework * feat: i18n * feat: tabs on data * feat: breadcrumbs clean
1 parent 10110d1 commit 7a61ede

File tree

18 files changed

+304
-306
lines changed

18 files changed

+304
-306
lines changed

packages/app-builder/src/components/CaseManager/DecisionPanel/DecisionPanel.tsx

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
2727
import { useTranslation } from 'react-i18next';
2828
import { filter, isNonNullish, map, pipe } from 'remeda';
2929
import { match } from 'ts-pattern';
30-
import { Button, Switch, Tabs, TabsContent, TabsList, TabsTrigger } from 'ui-design-system';
30+
import { Button, Switch, Tabs, tabClassName } from 'ui-design-system';
3131
import { Icon } from 'ui-icons';
3232

3333
type DecisionPanelProps = {
@@ -190,19 +190,39 @@ const ExpandedDetail = ({ decision, pivots, dataModel }: DetailProps & { dataMod
190190

191191
const CollapsedDetail = ({ decision, pivots, dataModel }: DetailProps & { dataModel: TableModel[] }) => {
192192
const { t } = useTranslation(casesI18n);
193+
const [activeTab, setActiveTab] = useState<'hits' | 'trigger'>('hits');
194+
193195
return (
194-
<Tabs defaultValue="hits" className="flex flex-col items-start gap-6">
195-
<TabsList>
196-
<TabsTrigger value="hits">{t('cases:decisions.rules')}</TabsTrigger>
197-
<TabsTrigger value="trigger">{t('cases:case_detail.trigger_object')}</TabsTrigger>
198-
</TabsList>
199-
<TabsContent value="hits" className="w-full">
200-
<DecisionRuleExecutions decision={decision} />
201-
</TabsContent>
202-
<TabsContent value="trigger" className="w-full">
203-
<DecisionTriggerObject decision={decision} pivots={pivots} dataModel={dataModel} />
204-
</TabsContent>
205-
</Tabs>
196+
<div className="flex flex-col items-start gap-6">
197+
<Tabs>
198+
<button
199+
type="button"
200+
className={tabClassName}
201+
data-status={activeTab === 'hits' ? 'active' : undefined}
202+
onClick={() => setActiveTab('hits')}
203+
>
204+
{t('cases:decisions.rules')}
205+
</button>
206+
<button
207+
type="button"
208+
className={tabClassName}
209+
data-status={activeTab === 'trigger' ? 'active' : undefined}
210+
onClick={() => setActiveTab('trigger')}
211+
>
212+
{t('cases:case_detail.trigger_object')}
213+
</button>
214+
</Tabs>
215+
{activeTab === 'hits' && (
216+
<div className="w-full">
217+
<DecisionRuleExecutions decision={decision} />
218+
</div>
219+
)}
220+
{activeTab === 'trigger' && (
221+
<div className="w-full">
222+
<DecisionTriggerObject decision={decision} pivots={pivots} dataModel={dataModel} />
223+
</div>
224+
)}
225+
</div>
206226
);
207227
};
208228

packages/app-builder/src/components/CaseManager/SnoozePanel/SnoozePanel.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import { useFormatDateTime, useFormatLanguage } from '@app-builder/utils/format'
1212
import { useLoaderData } from '@remix-run/react';
1313
import { Dict } from '@swan-io/boxed';
1414
import { formatRelative } from 'date-fns';
15-
import { useEffect } from 'react';
15+
import { useEffect, useState } from 'react';
1616
import { useTranslation } from 'react-i18next';
17-
import { Button, cn, Tabs, TabsContent, TabsList, TabsTrigger } from 'ui-design-system';
17+
import { Button, cn, Tabs, tabClassName } from 'ui-design-system';
1818
import { Icon } from 'ui-icons';
1919
import { DrawerContext } from '../Drawer/Drawer';
2020
import { PivotObjectDetails } from '../PivotsPanel/PivotsPanelContent';
@@ -35,6 +35,12 @@ export const SnoozePanel = ({
3535
const { setExpanded } = DrawerContext.useValue();
3636
const rulesByPivotQuery = useRulesByPivotQuery(caseDetail.id);
3737

38+
const pivotKeys = rulesByPivotQuery.data ? Object.keys(rulesByPivotQuery.data.rulesByPivot) : [];
39+
const [activeTab, setActiveTab] = useState<string | null>(null);
40+
41+
// Derive the effective active tab - use state if set, otherwise default to first pivot
42+
const effectiveActiveTab = activeTab ?? pivotKeys[0] ?? null;
43+
3844
useEffect(() => {
3945
setExpanded(true);
4046
}, [setExpanded]);
@@ -63,26 +69,29 @@ export const SnoozePanel = ({
6369

6470
<div className="flex w-full flex-col gap-6 px-2">
6571
<span className="text-l font-semibold">Rules</span>
66-
<Tabs className="flex w-full flex-col" defaultValue={Object.keys(rulesByPivot)[0]}>
67-
<TabsList className="mb-6 w-fit">
68-
{Object.keys(rulesByPivot).map((pivotValue) => {
72+
<div className="flex w-full flex-col">
73+
<Tabs>
74+
{pivotKeys.map((pivotValue) => {
6975
return (
70-
<TabsTrigger key={`trigger-${pivotValue}`} value={pivotValue} className="gap-2">
76+
<button
77+
key={`trigger-${pivotValue}`}
78+
type="button"
79+
className={cn(tabClassName, 'gap-2')}
80+
data-status={effectiveActiveTab === pivotValue ? 'active' : undefined}
81+
onClick={() => setActiveTab(pivotValue)}
82+
>
7183
<span className="font-medium">{pivotValue}</span>
72-
</TabsTrigger>
84+
</button>
7385
);
7486
})}
75-
</TabsList>
87+
</Tabs>
7688
{Dict.entries(rulesByPivot).map(([pivotValue, rules]) => {
89+
if (effectiveActiveTab !== pivotValue) return null;
7790
const client = findDataFromPivotValue(pivotObjects ?? [], pivotValue);
7891
const table = dataModelWithTableOptions.find((t) => t.name === client?.pivotObjectName);
7992

8093
return (
81-
<TabsContent
82-
className="flex w-full flex-col items-start gap-6"
83-
key={`content-${pivotValue}`}
84-
value={pivotValue}
85-
>
94+
<div className="mt-6 flex w-full flex-col items-start gap-6" key={`content-${pivotValue}`}>
8695
{table && client ? (
8796
<div className="border-grey-border flex flex-col gap-v2-md border p-v2-md bg-grey-background-light rounded-v2-lg">
8897
<div className="capitalize font-semibold">{table.name}</div>
@@ -178,10 +187,10 @@ export const SnoozePanel = ({
178187
);
179188
})}
180189
</div>
181-
</TabsContent>
190+
</div>
182191
);
183192
})}
184-
</Tabs>
193+
</div>
185194
</div>
186195
</div>
187196
);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getRoute } from '@app-builder/utils/routes';
2+
import { NavLink } from '@remix-run/react';
3+
import { useTranslation } from 'react-i18next';
4+
import { Tabs, tabClassName } from 'ui-design-system';
5+
import { Icon } from 'ui-icons';
6+
7+
export function DataTabs() {
8+
const { t } = useTranslation(['navigation']);
9+
10+
return (
11+
<Tabs>
12+
<NavLink to={getRoute('/data/list')} className={tabClassName}>
13+
<Icon icon="lists" className="mr-1 size-5" />
14+
{t('navigation:data.list')}
15+
</NavLink>
16+
<NavLink to={getRoute('/data/schema')} className={tabClassName}>
17+
<Icon icon="tree-schema" className="mr-1 size-5" />
18+
{t('navigation:data.schema')}
19+
</NavLink>
20+
<NavLink to={getRoute('/data/view')} className={tabClassName}>
21+
<Icon icon="visibility" className="mr-1 size-5" />
22+
{t('navigation:data.viewer')}
23+
</NavLink>
24+
</Tabs>
25+
);
26+
}

packages/app-builder/src/locales/ar/navigation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"case_manager.files": "الملفات",
99
"case_manager.hits": "التطابقات",
1010
"case_manager.information": "المعلومات",
11+
"cases.overview": "نظرة عامة",
1112
"collapse": "طيّ",
1213
"continuous_screening": "التحقق المستمر",
1314
"continuous-screening.configurations": "إعدادات التحقق المستمر",

packages/app-builder/src/locales/en/navigation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"case_manager.files": "Files",
99
"case_manager.hits": "Matches",
1010
"case_manager.information": "Information",
11+
"cases.overview": "Overview",
1112
"collapse": "Collapse",
1213
"continuous_screening": "Continuous Screening",
1314
"continuous-screening.configurations": "Continuous Screening Configurations",

packages/app-builder/src/locales/fr/navigation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"case_manager.files": "Fichiers",
99
"case_manager.hits": "Correspondances",
1010
"case_manager.information": "Informations",
11+
"cases.overview": "Vue d'ensemble",
1112
"collapse": "Réduire",
1213
"continuous_screening": "Monitoring permanent",
1314
"continuous-screening.configurations": "Configurations du monitoring permanent",

packages/app-builder/src/routes/_builder+/cases+/$caseId+/d+/$decisionId+/screenings+/$screeningId+/hits.tsx

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
1414
import { Fragment, useMemo, useState } from 'react';
1515
import { useTranslation } from 'react-i18next';
1616
import { match } from 'ts-pattern';
17-
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ui-design-system/src/Tabs/Tabs';
17+
import { Tabs, tabClassName } from 'ui-design-system';
1818
import { useCurrentCase } from './_layout';
1919

2020
export default function CaseSanctionsHitsPage() {
@@ -123,27 +123,46 @@ function ScreeningQueryDetail({
123123
const { t } = useTranslation(screeningsI18n);
124124
const processedQueries = Object.values(request.queries);
125125
const hasInitialQuery = Array.isArray(initialQuery) && initialQuery.length > 0;
126+
const [activeTab, setActiveTab] = useState<'initial' | 'preprocessed'>('preprocessed');
126127

127128
return (
128-
<Tabs defaultValue="preprocessed">
129-
<TabsList className="mb-2">
130-
{hasInitialQuery && <TabsTrigger value="initial">{t('screenings:initial_query')}</TabsTrigger>}
131-
<TabsTrigger value="preprocessed">
129+
<div>
130+
<Tabs>
131+
{hasInitialQuery && (
132+
<button
133+
type="button"
134+
className={tabClassName}
135+
data-status={activeTab === 'initial' ? 'active' : undefined}
136+
onClick={() => setActiveTab('initial')}
137+
>
138+
{t('screenings:initial_query')}
139+
</button>
140+
)}
141+
<button
142+
type="button"
143+
className={tabClassName}
144+
data-status={activeTab === 'preprocessed' ? 'active' : undefined}
145+
onClick={() => setActiveTab('preprocessed')}
146+
>
132147
{!hasInitialQuery ? t('screenings:query') : t('screenings:processed_query')}
133-
</TabsTrigger>
134-
</TabsList>
135-
{hasInitialQuery && (
136-
<TabsContent value="initial">
137-
{initialQuery.map((q, i) => (
138-
<QueryObjectDetail key={i} query={q as ScreeningQuery} />
139-
))}
140-
</TabsContent>
141-
)}
142-
<TabsContent value="preprocessed">
143-
{processedQueries.map((q, i) => (
144-
<QueryObjectDetail key={i} query={q as ScreeningQuery} />
145-
))}
146-
</TabsContent>
147-
</Tabs>
148+
</button>
149+
</Tabs>
150+
<div className="mt-2">
151+
{activeTab === 'initial' && hasInitialQuery && (
152+
<>
153+
{initialQuery.map((q, i) => (
154+
<QueryObjectDetail key={i} query={q as ScreeningQuery} />
155+
))}
156+
</>
157+
)}
158+
{activeTab === 'preprocessed' && (
159+
<>
160+
{processedQueries.map((q, i) => (
161+
<QueryObjectDetail key={i} query={q as ScreeningQuery} />
162+
))}
163+
</>
164+
)}
165+
</div>
166+
</div>
148167
);
149168
}

packages/app-builder/src/routes/_builder+/cases+/_detail+/s.$caseId.tsx

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,7 @@ import { useTranslation } from 'react-i18next';
3030
import * as R from 'remeda';
3131
import { ClientOnly } from 'remix-utils/client-only';
3232
import { match } from 'ts-pattern';
33-
import {
34-
Button,
35-
ButtonV2,
36-
CtaV2ClassName,
37-
cn,
38-
Markdown,
39-
Tabs,
40-
TabsContent,
41-
TabsList,
42-
TabsTrigger,
43-
} from 'ui-design-system';
33+
import { Button, ButtonV2, CtaV2ClassName, cn, Markdown, Tabs, tabClassName } from 'ui-design-system';
4434
import { Icon } from 'ui-icons';
4535

4636
export const loader = createServerFn(
@@ -131,6 +121,7 @@ export default function CaseManagerIndexPage() {
131121
const [drawerContentMode, setDrawerContentMode] = useState<'pivot' | 'decision' | 'snooze'>('pivot');
132122
const enqueueReviewMutation = useEnqueueCaseReviewMutation();
133123
const [hasRequestedReview, setHasRequestedReview] = useState(false);
124+
const [activeReviewTab, setActiveReviewTab] = useState<'review' | 'sanityCheck'>('review');
134125

135126
useEffect(() => {
136127
if (isMenuExpanded) {
@@ -163,9 +154,14 @@ export default function CaseManagerIndexPage() {
163154
? (() => {
164155
return (
165156
<div className="flex flex-col gap-2 h-full text-default">
166-
<Tabs defaultValue="review" className="flex flex-col h-full gap-2">
167-
<TabsList className="self-start">
168-
<TabsTrigger value="review" className="flex items-center gap-2">
157+
<div className="flex flex-col h-full gap-2">
158+
<Tabs>
159+
<button
160+
type="button"
161+
className={cn(tabClassName, 'gap-2')}
162+
data-status={activeReviewTab === 'review' ? 'active' : undefined}
163+
onClick={() => setActiveReviewTab('review')}
164+
>
169165
{t('cases:case.ai_assist.review')}
170166
<Icon
171167
icon={mostRecentReview.review.ok ? 'tick' : 'cross'}
@@ -174,22 +170,29 @@ export default function CaseManagerIndexPage() {
174170
mostRecentReview.review.ok ? 'text-green-primary' : 'text-red-primary',
175171
)}
176172
/>
177-
</TabsTrigger>
173+
</button>
178174
{!mostRecentReview.review.ok ? (
179-
<TabsTrigger value="sanityCheck">
175+
<button
176+
type="button"
177+
className={tabClassName}
178+
data-status={activeReviewTab === 'sanityCheck' ? 'active' : undefined}
179+
onClick={() => setActiveReviewTab('sanityCheck')}
180+
>
180181
{t('cases:case.ai_assist.sanity_check')}
181-
</TabsTrigger>
182+
</button>
182183
) : null}
183-
</TabsList>
184-
<TabsContent value="review" className="min-h-0 p-2 overflow-scroll">
185-
<Markdown>{mostRecentReview.review.output}</Markdown>
186-
</TabsContent>
187-
{!mostRecentReview.ok ? (
188-
<TabsContent value="sanityCheck" className="min-h-0 p-2 overflow-scroll">
184+
</Tabs>
185+
{activeReviewTab === 'review' && (
186+
<div className="min-h-0 p-2 overflow-scroll">
187+
<Markdown>{mostRecentReview.review.output}</Markdown>
188+
</div>
189+
)}
190+
{activeReviewTab === 'sanityCheck' && !mostRecentReview.ok && (
191+
<div className="min-h-0 p-2 overflow-scroll">
189192
<Markdown>{mostRecentReview.review.sanityCheck}</Markdown>
190-
</TabsContent>
191-
) : null}
192-
</Tabs>
193+
</div>
194+
)}
195+
</div>
193196
</div>
194197
);
195198
})()
@@ -237,7 +240,6 @@ export default function CaseManagerIndexPage() {
237240
</div>
238241
</Page.Header>
239242
<Page.Container className="text-default relative h-full flex-row p-0 lg:p-0">
240-
{/* TabSystem when mostRecentReview is not empty */}
241243
<CaseDetails
242244
key={details.id}
243245
currentUser={currentUser}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { BreadCrumbLink, type BreadCrumbProps } from '@app-builder/components/Breadcrumbs';
2+
import { getRoute } from '@app-builder/utils/routes';
3+
import { Outlet } from '@remix-run/react';
4+
import { type Namespace } from 'i18next';
5+
import { useTranslation } from 'react-i18next';
6+
import { Icon } from 'ui-icons';
7+
8+
export const handle = {
9+
i18n: ['navigation'] satisfies Namespace,
10+
BreadCrumbs: [
11+
({ isLast }: BreadCrumbProps) => {
12+
const { t } = useTranslation(['navigation']);
13+
return (
14+
<BreadCrumbLink to={getRoute('/cases')} isLast={isLast}>
15+
<Icon icon="case-manager" className="me-2 size-6" />
16+
{t('navigation:case_manager')}
17+
</BreadCrumbLink>
18+
);
19+
},
20+
],
21+
};
22+
23+
export default function CasesLayout() {
24+
return <Outlet />;
25+
}

0 commit comments

Comments
 (0)