Add shared Show primitive
- move the conditional rendering helper into components/primitives - use it in the issues board and issue domain host - keep the issue page and host easier to scan without repeated null branches
This commit is contained in:
parent
a4a4c0f12d
commit
adb6426a2f
5 changed files with 98 additions and 34 deletions
1
webui/src/components/primitives/index.ts
Normal file
1
webui/src/components/primitives/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Show } from './show';
|
||||
33
webui/src/components/primitives/show.test.tsx
Normal file
33
webui/src/components/primitives/show.test.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { Show } from './show';
|
||||
|
||||
describe('Show', () => {
|
||||
it('renders children when the condition is true', () => {
|
||||
render(
|
||||
<Show when={true}>
|
||||
<span>Visible</span>
|
||||
</Show>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Visible')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders fallback when the condition is false', () => {
|
||||
render(
|
||||
<Show fallback={<span>Hidden</span>} when={false}>
|
||||
<span>Visible</span>
|
||||
</Show>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Hidden')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Visible')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports render-prop children', () => {
|
||||
render(<Show when="Ada">{(name) => <span>{name}</span>}</Show>);
|
||||
|
||||
expect(screen.getByText('Ada')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
23
webui/src/components/primitives/show.tsx
Normal file
23
webui/src/components/primitives/show.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { ReactNode } from 'react';
|
||||
|
||||
type ShowChildren<T> = ReactNode | ((value: NonNullable<T>) => ReactNode);
|
||||
|
||||
export function Show<T>({
|
||||
fallback = null,
|
||||
children,
|
||||
when,
|
||||
}: {
|
||||
children: ShowChildren<T>;
|
||||
fallback?: ReactNode;
|
||||
when: T;
|
||||
}) {
|
||||
if (!when) {
|
||||
return <>{fallback}</>;
|
||||
}
|
||||
|
||||
if (typeof children === 'function') {
|
||||
return <>{(children as (value: NonNullable<T>) => ReactNode)(when as NonNullable<T>)}</>;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import {
|
|||
TextArea,
|
||||
TextInput,
|
||||
} from '@/components/form';
|
||||
import { Show } from '@/components/primitives';
|
||||
import { useProfile } from '@/platform/shell/route-controllers';
|
||||
|
||||
import type { IssuePriority, IssueReportPayload } from '../-issues.types';
|
||||
|
|
@ -23,6 +24,7 @@ import {
|
|||
REFRESH_EVENT,
|
||||
createDefaultIssueTitle,
|
||||
getIssueCategoriesForEntity,
|
||||
getEntityLabel,
|
||||
} from '../-issues.helpers';
|
||||
import styles from './issue-detail-modal.module.css';
|
||||
|
||||
|
|
@ -89,19 +91,21 @@ export function IssueDomainHost() {
|
|||
};
|
||||
}, [queryClient]);
|
||||
|
||||
if (!reportPayload) return null;
|
||||
|
||||
return (
|
||||
<ReportIssueModal
|
||||
key={`${reportPayload.entityType}:${reportPayload.entityId}`}
|
||||
payload={reportPayload}
|
||||
profileId={profileId}
|
||||
onClose={() => setReportPayload(null)}
|
||||
onSubmitted={() => {
|
||||
setReportPayload(null);
|
||||
void queryClient.invalidateQueries({ queryKey: ISSUE_DOMAIN_QUERY_KEY });
|
||||
}}
|
||||
/>
|
||||
<Show when={reportPayload}>
|
||||
{(payload) => (
|
||||
<ReportIssueModal
|
||||
key={`${payload.entityType}:${payload.entityId}`}
|
||||
payload={payload}
|
||||
profileId={profileId}
|
||||
onClose={() => setReportPayload(null)}
|
||||
onSubmitted={() => {
|
||||
setReportPayload(null);
|
||||
void queryClient.invalidateQueries({ queryKey: ISSUE_DOMAIN_QUERY_KEY });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -120,8 +124,6 @@ function ReportIssueModal({
|
|||
() => getIssueCategoriesForEntity(payload.entityType),
|
||||
[payload.entityType],
|
||||
);
|
||||
const entityLabel =
|
||||
payload.entityType === 'track' ? 'Track' : payload.entityType === 'album' ? 'Album' : 'Artist';
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (values: ReportIssueFormValues) => {
|
||||
|
|
@ -172,9 +174,11 @@ function ReportIssueModal({
|
|||
}
|
||||
}}
|
||||
className={styles.reportIssueDialog}
|
||||
closeLabel="Close report issue modal"
|
||||
>
|
||||
<DialogHeader title={`Report Issue - ${entityLabel}`} closeLabel="Close report issue modal" />
|
||||
<DialogHeader
|
||||
title={`Report Issue - ${getEntityLabel(payload.entityType)}`}
|
||||
closeLabel="Close report issue modal"
|
||||
/>
|
||||
<DialogBody>
|
||||
<form
|
||||
className={styles.reportIssueForm}
|
||||
|
|
@ -186,12 +190,14 @@ function ReportIssueModal({
|
|||
>
|
||||
<div className={styles.reportIssueEntityInfo}>
|
||||
<div className={styles.reportIssueEntityName}>{payload.entityName}</div>
|
||||
{payload.artistName ? (
|
||||
<div className={styles.reportIssueEntityArtist}>
|
||||
{payload.artistName}
|
||||
{payload.albumTitle ? ` - ${payload.albumTitle}` : ''}
|
||||
</div>
|
||||
) : null}
|
||||
<Show when={payload.artistName}>
|
||||
{(artistName) => (
|
||||
<div className={styles.reportIssueEntityArtist}>
|
||||
{artistName}
|
||||
<Show when={payload.albumTitle}>{(albumTitle) => ` - ${albumTitle}`}</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
|
|
@ -228,8 +234,8 @@ function ReportIssueModal({
|
|||
</FormField>
|
||||
|
||||
<form.Subscribe selector={(state) => state.values.category}>
|
||||
{(selectedCategory) =>
|
||||
selectedCategory ? (
|
||||
{(selectedCategory) => (
|
||||
<Show when={selectedCategory}>
|
||||
<>
|
||||
<form.Field name="title">
|
||||
{(field) => (
|
||||
|
|
@ -296,8 +302,8 @@ function ReportIssueModal({
|
|||
)}
|
||||
</form.Field>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
</Show>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
|
||||
<form.Subscribe selector={(state) => state.errors}>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useNavigate } from '@tanstack/react-router';
|
|||
import { useEffect } from 'react';
|
||||
|
||||
import { Select } from '@/components/form';
|
||||
import { Show } from '@/components/primitives';
|
||||
import { useProfile, useReactPageShell } from '@/platform/shell/route-controllers';
|
||||
|
||||
import type { IssueCounts, IssueRecord, IssueStatus } from '../-issues.types';
|
||||
|
|
@ -350,27 +351,27 @@ function IssueBoardCard({
|
|||
{catMeta.icon}
|
||||
</span>
|
||||
<span className={styles.issueCardTitle}>{issue.title}</span>
|
||||
{issue.admin_response ? (
|
||||
<Show when={issue.admin_response}>
|
||||
<span className={styles.issueCardResponded} title="Admin has responded">
|
||||
💬
|
||||
</span>
|
||||
) : null}
|
||||
</Show>
|
||||
</div>
|
||||
<div className={styles.issueCardEntity}>
|
||||
<span className={styles.issueCardEntityType}>{getEntityLabel(issue.entity_type)}</span>
|
||||
<span className={styles.issueCardEntityName}>{entityName}</span>
|
||||
{details.length > 0 ? (
|
||||
<Show when={details.length}>
|
||||
<span className={styles.issueCardMetaLine}>{details.join(' - ')}</span>
|
||||
) : null}
|
||||
</Show>
|
||||
</div>
|
||||
{issue.description ? (
|
||||
<Show when={issue.description}>
|
||||
<div className={styles.issueCardDescription}>{issue.description}</div>
|
||||
) : null}
|
||||
</Show>
|
||||
<div className={styles.issueCardFooter}>
|
||||
<span className={styles.issueCardDate}>{createdDate}</span>
|
||||
{showReporterName && issue.reporter_name ? (
|
||||
<Show when={showReporterName && issue.reporter_name}>
|
||||
<span className={styles.issueCardProfile}>by {issue.reporter_name}</span>
|
||||
) : null}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.issueCardRight}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue