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
pull/388/head
Antti Kettunen 1 month ago
parent a4a4c0f12d
commit adb6426a2f
No known key found for this signature in database
GPG Key ID: C6B2A3D250359BD7

@ -0,0 +1 @@
export { Show } from './show';

@ -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();
});
});

@ -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…
Cancel
Save