mirror of
https://github.com/dsh-market/dsh-market.git
synced 2026-09-28 05:03:07 +08:00
refactor(client): the host's Checkbox where its string label fits
Two of the market's five checkboxes are a plain box plus a short label (Advanced → automatic backup; the export dialog's "include configuration"), and those now render the host's `Checkbox` — matching the settings page around them. Older hosts keep the market's label+input. The other three deliberately do not, and the reasons are written down where the next person will ask: - the export picker's per-plugin rows carry rich label content (name, spec kind, resolved spec), and `Checkbox` takes a STRING label — routing them through it would either drop that content or print the name twice; - the recovery panel's checkbox has no visible label at all; - the gist mode picker is a pair of RADIOS, and the host ships no radio primitive (0.1.7-rc.2: Checkbox, Switch, SegmentedControl, Tag — no radio). Verified: 1808 unit tests, npm run check clean, and a mutation that ignores the host component turns the new test red.
This commit is contained in:
+29
-12
File diff suppressed because one or more lines are too long
@@ -36,7 +36,7 @@ import {
|
||||
IconSparkle16,
|
||||
IconWarningOutline16,
|
||||
} from './icons.ts'
|
||||
import { HostSwitch, HostTag } from './optional-primitives.ts'
|
||||
import { HostCheckbox, HostSwitch, HostTag } from './optional-primitives.ts'
|
||||
import css from './Market.module.css'
|
||||
import { MARK_BLOCK_RADIUS, MARK_BLOCK_SIZE, MARK_GRID_BLOCKS, MARK_PLUG_BLOCK, MARK_VIEW_BOX } from './market-mark.ts'
|
||||
import { CommentsModal } from './CommentsModal.tsx'
|
||||
@@ -4011,6 +4011,20 @@ export function MarketSection(props: MarketSectionProps) {
|
||||
</button>
|
||||
))
|
||||
|
||||
/**
|
||||
* A labelled checkbox: the host's when it has one, the market's label+input
|
||||
* otherwise. Only the simple ones go through here — see `HostCheckbox` for
|
||||
* why the export rows and the recovery panel keep their own markup.
|
||||
*/
|
||||
const labelledCheckbox = (opts: { label: string; checked: boolean; disabled?: boolean; className?: string; onChange: (next: boolean) => void }) => (HostCheckbox !== null
|
||||
? <HostCheckbox checked={opts.checked} onChange={opts.onChange} label={opts.label} disabled={opts.disabled} className={opts.className} />
|
||||
: (
|
||||
<label className={opts.className}>
|
||||
<input type="checkbox" checked={opts.checked} disabled={opts.disabled} onChange={event => opts.onChange(event.target.checked)} />
|
||||
{opts.label}
|
||||
</label>
|
||||
))
|
||||
|
||||
const pluginCard = (p: RegistryPlugin) => {
|
||||
const desc = (p.description && (p.description[lang] || p.description.en)) || ''
|
||||
const done = doneUrls.includes(p.url) || hotUrls.includes(p.url)
|
||||
@@ -4921,7 +4935,12 @@ export function MarketSection(props: MarketSectionProps) {
|
||||
<Button variant="primary" size="sm" disabled={backupBusy || webdavUrl.trim() === ''} onClick={() => runWebdav('backup')}>{backupBusy ? t('backupWorking') : t('webdavUpload')}</Button>
|
||||
<Button variant="outline" size="sm" disabled={backupBusy || webdavUrl.trim() === ''} onClick={() => runWebdav('restore')}>{t('webdavRestore')}</Button>
|
||||
</div>
|
||||
<label className={css.backupCheck}><input type="checkbox" checked={autoBackup} onChange={e => setAutoBackup(e.target.checked)} />{t('autoBackup')}</label>
|
||||
{labelledCheckbox({
|
||||
label: t('autoBackup'),
|
||||
checked: autoBackup,
|
||||
className: css.backupCheck,
|
||||
onChange: setAutoBackup,
|
||||
})}
|
||||
<p>{t('webdavNote')}</p>
|
||||
<p className={css.backupWarn}>{t('credsWarning')}</p>
|
||||
</section>
|
||||
@@ -6394,10 +6413,12 @@ export function MarketSection(props: MarketSectionProps) {
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<label className={css.backupCheck}>
|
||||
<input type="checkbox" checked={exportIncludeConfig} onChange={e => setExportIncludeConfig(e.target.checked)} />
|
||||
{t('gistIncludeConfig')}
|
||||
</label>
|
||||
{labelledCheckbox({
|
||||
label: t('gistIncludeConfig'),
|
||||
checked: exportIncludeConfig,
|
||||
className: css.backupCheck,
|
||||
onChange: setExportIncludeConfig,
|
||||
})}
|
||||
{exportIncludeConfig && <p className={css.backupWarn}>{t('credsWarning')}</p>}
|
||||
{exportError !== null && <p className={css.backupWarn}>{exportError}</p>}
|
||||
</>
|
||||
|
||||
@@ -68,3 +68,23 @@ export type SwitchComponent = (props: {
|
||||
}) => ReactNode
|
||||
|
||||
export const HostSwitch: SwitchComponent | null = optionalComponent<SwitchComponent>('Switch')
|
||||
|
||||
/**
|
||||
* `Checkbox` — the host's labelled checkbox. Null before 0.1.7-rc.2.
|
||||
*
|
||||
* It takes a STRING label, which is what limits its use here: the market's
|
||||
* checkboxes that carry rich label content (the export rows: name, spec kind,
|
||||
* resolved spec) or no visible label at all (the recovery panel's) stay native
|
||||
* markup — routing those through this would either lose the row's content or
|
||||
* duplicate it.
|
||||
*/
|
||||
export type CheckboxComponent = (props: {
|
||||
checked: boolean
|
||||
onChange: (next: boolean) => void
|
||||
label: string
|
||||
disabled?: boolean
|
||||
title?: string
|
||||
className?: string
|
||||
}) => ReactNode
|
||||
|
||||
export const HostCheckbox: CheckboxComponent | null = optionalComponent<CheckboxComponent>('Checkbox')
|
||||
|
||||
@@ -20,6 +20,8 @@ vi.mock('@deepseek-ai/dsh-client-ui-primitives', async (importOriginal) => ({
|
||||
<span data-host-tag={tone ?? 'outline'}>{children}</span>,
|
||||
Switch: ({ checked, label }: { checked: boolean; label: string }) =>
|
||||
<button type="button" data-host-switch={String(checked)} aria-label={label} />,
|
||||
Checkbox: ({ checked, label }: { checked: boolean; label: string }) =>
|
||||
<label data-host-checkbox={String(checked)}>{label}</label>,
|
||||
}))
|
||||
|
||||
import { MarketSection, resetMarketPortalHost } from '../../src/client/MarketSection.tsx'
|
||||
@@ -104,3 +106,31 @@ describe('the on/off control on a host that has Switch', () => {
|
||||
expect(switches[0]!.getAttribute('aria-label')).toMatch(/Disable dsh-tagged/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('labelled checkboxes on a host that has Checkbox', () => {
|
||||
it('uses the host checkbox for the market simple labelled ones', async () => {
|
||||
// The Advanced tab's auto-backup flag is a plain labelled checkbox, which
|
||||
// is exactly what the host's component is for. Its rich-label siblings
|
||||
// (the export rows) keep native markup — `HostCheckbox` says why.
|
||||
vi.stubGlobal('fetch', vi.fn((input: unknown) => {
|
||||
const path = String(input).split('?')[0]
|
||||
if (path === '/dsh-market/registry') return Promise.resolve(new Response(JSON.stringify({ source: 'live', hostVersion: '0.1.2-alpha.2', registry: REGISTRY })))
|
||||
if (path === '/dsh-market/installed') return Promise.resolve(new Response(JSON.stringify({ profile: 'web', installed: {}, live: [], disabled: [], groups: {}, groupOrder: [], favorites: [] })))
|
||||
if (path === '/dsh-market/status') return Promise.resolve(new Response(JSON.stringify({ active: false, pnpm: true, boot: 'boot-1', restart: true, installed: {} })))
|
||||
if (path === '/dsh-market/updates') return Promise.resolve(new Response(JSON.stringify({ updates: {} })))
|
||||
return Promise.reject(new Error(`unstubbed fetch: ${path}`))
|
||||
}))
|
||||
render(<MarketSection {...props()} />)
|
||||
await screen.findByText('dsh-tagged')
|
||||
const advanced = screen.getAllByRole('button', { name: /^(Advanced|高级)$/u })
|
||||
.find(button => /(^|_)tab(_|$)/u.test(button.className))
|
||||
expect(advanced, 'no Advanced tab').toBeTruthy()
|
||||
const { fireEvent } = await import('@testing-library/react')
|
||||
fireEvent.click(advanced!)
|
||||
const boxes = await screen.findAllByText(en.autoBackup)
|
||||
expect(boxes.length).toBeGreaterThan(0)
|
||||
const host = document.querySelector('[data-host-checkbox]')
|
||||
expect(host).toBeTruthy()
|
||||
expect(host!.textContent).toBe(en.autoBackup)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user