fix: wait for file associations before enabling the default action
This commit is contained in:
1 parent
6111f879fd
commit
71b7bbeb88
3 files changed
+27
-7
No files matched your search
@@ -58,7 +58,8 @@ export function FileOpenTarget(props: FileOpenTargetProps): ReactNode {
|
||||
return (
|
||||
<OpenTargetButton
|
||||
key={props.absolutePath} kind="file" applications={apps ?? []} defaultId={apps?.find(app => app.default)?.id}
|
||||
failed={association?.path === props.absolutePath && apps === null} empty={props.empty === true} t={props.t}
|
||||
loading={association?.path !== props.absolutePath} failed={association?.path === props.absolutePath && apps === null}
|
||||
empty={props.empty === true} t={props.t}
|
||||
refresh={() => { setRevision(value => value + 1) }}
|
||||
execute={operation => props.openPath(props.absolutePath, operation.kind === 'reveal' ? 'reveal' : 'open',
|
||||
operation.kind === 'application' ? operation.id : undefined)}
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface OpenTargetButtonProps {
|
||||
readonly applications: readonly OpenTargetApplication[]
|
||||
readonly defaultId: string | undefined
|
||||
readonly failed: boolean
|
||||
readonly loading?: boolean
|
||||
readonly empty?: boolean
|
||||
readonly t: TranslateNS<typeof NS>
|
||||
readonly execute: (operation: OpenTargetOperation) => Promise<OpenInAppPathFailure | null>
|
||||
@@ -78,6 +79,7 @@ export function OpenTargetButton(props: OpenTargetButtonProps): ReactNode {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const { pending, toast, act } = useOpenTargetGesture(props.execute, t)
|
||||
const preferred = applications.find(app => app.id === defaultId)
|
||||
const disabled = pending || props.loading === true
|
||||
const revealDefault = kind === 'file' && preferred === undefined
|
||||
const primaryLabel = preferred === undefined ? t('path.reveal') : t('open.title', { app: preferred.name })
|
||||
const run = (operation: OpenTargetOperation): void => { setMenuOpen(false); act(operation) }
|
||||
@@ -87,7 +89,7 @@ export function OpenTargetButton(props: OpenTargetButtonProps): ReactNode {
|
||||
: <ApplicationIcon key={preferred?.icon} source={preferred?.icon ?? null} size={13} />
|
||||
if (props.empty === true) return (
|
||||
<>
|
||||
<button type="button" className={css.empty} disabled={pending} data-open-path-unpreviewable onClick={primary}>
|
||||
<button type="button" className={css.empty} disabled={disabled} data-open-path-unpreviewable onClick={primary}>
|
||||
{t('path.open')}{icon}
|
||||
</button>
|
||||
{toast}
|
||||
@@ -97,7 +99,7 @@ export function OpenTargetButton(props: OpenTargetButtonProps): ReactNode {
|
||||
<>
|
||||
<Menu
|
||||
className={css.menuAnchor}
|
||||
open={menuOpen && !pending}
|
||||
open={menuOpen && !disabled}
|
||||
autoFocus
|
||||
portal
|
||||
dense
|
||||
@@ -117,15 +119,15 @@ export function OpenTargetButton(props: OpenTargetButtonProps): ReactNode {
|
||||
]}
|
||||
onSelect={(id) => { run(id === 'reveal' ? { kind: 'reveal' } : { kind: 'application', id: id.slice(4) }) }}
|
||||
anchor={(
|
||||
<div className={css.split} data-open-target={kind} data-open-path={kind === 'file' ? '' : undefined} data-state={pending ? 'busy' : 'idle'}>
|
||||
<div className={css.split} data-open-target={kind} data-open-path={kind === 'file' ? '' : undefined} data-state={disabled ? 'busy' : 'idle'}>
|
||||
<Tooltip label={revealDefault ? t('path.reveal') : t('open.tooltip')} side="bottom" delayMs={500}>
|
||||
<button type="button" className={css.main} disabled={pending} aria-label={primaryLabel} data-open-path-open={kind === 'file' ? '' : undefined} onClick={primary}>
|
||||
<button type="button" className={css.main} disabled={disabled} aria-label={primaryLabel} data-open-path-open={kind === 'file' ? '' : undefined} onClick={primary}>
|
||||
{icon}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
type="button" className={css.chevron} disabled={pending}
|
||||
aria-haspopup="menu" aria-expanded={menuOpen && !pending} aria-label={t('path.more')}
|
||||
type="button" className={css.chevron} disabled={disabled}
|
||||
aria-haspopup="menu" aria-expanded={menuOpen && !disabled} aria-label={t('path.more')}
|
||||
data-open-path-more={kind === 'file' ? '' : undefined}
|
||||
onClick={() => {
|
||||
if (!menuOpen) props.refresh?.()
|
||||
|
||||
@@ -187,6 +187,7 @@ it('ignores an obsolete file query and refreshes the default application when th
|
||||
it('keeps reveal last and makes it the default when the file query fails', async () => {
|
||||
const b = bench()
|
||||
render(<OpenPathAction {...b.props} applications={async () => null} />)
|
||||
await act(async () => {})
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: zh['path.more'] })) })
|
||||
const items = screen.getAllByRole('menuitem')
|
||||
expect(items.map(item => item.textContent)).toEqual([zh['path.appsError'], '显示文件位置(默认)'])
|
||||
@@ -198,6 +199,7 @@ it('keeps reveal last and makes it the default when the file query fails', async
|
||||
it.each([{ apps: [] }, { apps: [{ id: '/Player.app', name: 'Player', default: false, icon: null }] }])('uses reveal as the default after the available applications when none is default', async ({ apps }) => {
|
||||
const b = bench()
|
||||
render(<OpenPathAction {...b.props} applications={async () => apps} />)
|
||||
await act(async () => {})
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: zh['path.more'] })) })
|
||||
expect(screen.getAllByRole('menuitem').at(-1)?.textContent).toBe('显示文件位置(默认)')
|
||||
expect(screen.queryByText('用默认应用打开')).toBeNull()
|
||||
@@ -230,3 +232,18 @@ it('closes the application menu from its chevron without querying again', async
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
expect(b.props.applications).toHaveBeenCalledTimes(calls)
|
||||
})
|
||||
|
||||
|
||||
it('waits for the file association query before allowing a default action', async () => {
|
||||
const b = bench()
|
||||
const query = Promise.withResolvers<Awaited<ReturnType<OpenPathActionProps['applications']>>>()
|
||||
const view = render(<OpenPathAction {...b.props} applications={() => query.promise} />)
|
||||
const main = view.container.querySelector('[data-open-path-open]')!
|
||||
expect(main).toHaveProperty('disabled', true)
|
||||
fireEvent.click(main)
|
||||
expect(b.openPath).not.toHaveBeenCalled()
|
||||
await act(async () => { query.resolve([]) })
|
||||
expect(main).toHaveProperty('disabled', false)
|
||||
await act(async () => { fireEvent.click(main) })
|
||||
expect(b.openPath).toHaveBeenLastCalledWith(ABSOLUTE_PATH, 'reveal', undefined)
|
||||
})
|
||||
Reference in new issue
Block a user