refactor: simplify useRegisterCommand types (#1678)

This commit is contained in:
Whitewater 2024-11-21 14:41:49 +08:00 committed by GitHub
parent 2158b1cc25
commit 840c49e8fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 187 additions and 160 deletions

View File

@ -13,8 +13,7 @@ import { tipcClient } from "~/lib/client"
import { useTipModal } from "~/modules/wallet/hooks"
import { entryActions, useEntryStore } from "~/store/entry"
import { useRegisterCommandEffect } from "../hooks/use-register-command-effect"
import { defineFollowCommand } from "../registry/command"
import { useRegisterFollowCommand } from "../hooks/use-register-command"
import { COMMAND_ID } from "./id"
const useCollect = () => {
@ -89,8 +88,8 @@ export const useRegisterEntryCommands = () => {
const read = useRead()
const unread = useUnread()
useRegisterCommandEffect([
defineFollowCommand({
useRegisterFollowCommand([
{
id: COMMAND_ID.entry.tip,
label: t("entry_actions.tip"),
icon: <i className="i-mgc-power-outline" />,
@ -105,8 +104,8 @@ export const useRegisterEntryCommands = () => {
}),
)
},
}),
defineFollowCommand({
},
{
id: COMMAND_ID.entry.star,
label: t("entry_actions.star"),
icon: <i className="i-mgc-star-cute-re" />,
@ -127,8 +126,8 @@ export const useRegisterEntryCommands = () => {
// }
collect.mutate({ entryId, view })
},
}),
defineFollowCommand({
},
{
id: COMMAND_ID.entry.unstar,
label: t("entry_actions.unstar"),
icon: <i className="i-mgc-star-cute-fi text-orange-500" />,
@ -140,8 +139,8 @@ export const useRegisterEntryCommands = () => {
}
uncollect.mutate(entry.entries.id)
},
}),
defineFollowCommand({
},
{
id: COMMAND_ID.entry.delete,
label: t("entry_actions.delete"),
icon: <i className="i-mgc-delete-2-cute-re" />,
@ -153,8 +152,8 @@ export const useRegisterEntryCommands = () => {
}
deleteInboxEntry.mutate(entry.entries.id)
},
}),
defineFollowCommand({
},
{
id: COMMAND_ID.entry.copyLink,
label: t("entry_actions.copy_link"),
icon: <i className="i-mgc-link-cute-re" />,
@ -170,8 +169,8 @@ export const useRegisterEntryCommands = () => {
duration: 1000,
})
},
}),
defineFollowCommand({
},
{
id: COMMAND_ID.entry.copyTitle,
label: t("entry_actions.copy_title"),
icon: <i className="i-mgc-copy-cute-re" />,
@ -187,8 +186,8 @@ export const useRegisterEntryCommands = () => {
duration: 1000,
})
},
}),
defineFollowCommand({
},
{
id: COMMAND_ID.entry.openInBrowser,
label: t("entry_actions.open_in_browser", {
which: t(IN_ELECTRON ? "words.browser" : "words.newTab"),
@ -202,8 +201,8 @@ export const useRegisterEntryCommands = () => {
}
window.open(entry.entries.url, "_blank")
},
}),
defineFollowCommand({
},
{
id: COMMAND_ID.entry.viewSourceContent,
label: t("entry_actions.view_source_content"),
icon: <i className="i-mgc-world-2-cute-re" />,
@ -232,16 +231,16 @@ export const useRegisterEntryCommands = () => {
}
setShowSourceContent(true)
},
}),
defineFollowCommand({
},
{
id: COMMAND_ID.entry.viewEntryContent,
label: t("entry_actions.view_source_content"),
icon: <i className="i-mgc-world-2-cute-fi" />,
run: () => {
setShowSourceContent(false)
},
}),
defineFollowCommand({
},
{
id: COMMAND_ID.entry.share,
label: t("entry_actions.share"),
icon:
@ -267,8 +266,8 @@ export const useRegisterEntryCommands = () => {
}
return
},
}),
defineFollowCommand({
},
{
id: COMMAND_ID.entry.read,
label: t("entry_actions.mark_as_read"),
icon: <i className="i-mgc-round-cute-fi" />,
@ -280,8 +279,8 @@ export const useRegisterEntryCommands = () => {
}
read.mutate({ entryId, feedId: entry.feedId })
},
}),
defineFollowCommand({
},
{
id: COMMAND_ID.entry.unread,
label: t("entry_actions.mark_as_unread"),
icon: <i className="i-mgc-round-cute-re" />,
@ -293,6 +292,6 @@ export const useRegisterEntryCommands = () => {
}
unread.mutate({ entryId, feedId: entry.feedId })
},
}),
},
])
}

View File

@ -21,7 +21,7 @@ import { parseHtml } from "~/lib/parse-html"
import type { FlatEntryModel } from "~/store/entry"
import { useEntryStore } from "~/store/entry"
import { useRegisterCommandEffect } from "../hooks/use-register-command-effect"
import { useRegisterCommandEffect } from "../hooks/use-register-command"
import { defineFollowCommand } from "../registry/command"
import { COMMAND_ID } from "./id"

View File

@ -9,7 +9,7 @@ import { getRouteParams } from "~/hooks/biz/useRouteParams"
import { useDeleteSubscription } from "~/hooks/biz/useSubscriptionActions"
import { ListForm } from "~/modules/discover/list-form"
import { useRegisterCommandEffect } from "../hooks/use-register-command-effect"
import { useRegisterCommandEffect } from "../hooks/use-register-command"
import { COMMAND_ID } from "./id"
export const useRegisterListCommands = () => {

View File

@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"
import { useSetTheme } from "~/hooks/common"
import { useRegisterCommandEffect } from "../hooks/use-register-command-effect"
import { useRegisterCommandEffect } from "../hooks/use-register-command"
export const useRegisterThemeCommands = () => {
const [t] = useTranslation("settings")

View File

@ -1,33 +0,0 @@
import { useEffect } from "react"
import { registerCommand } from "../registry/registry"
import type { CommandOptions } from "../types"
export type RegisterOptions = {
deps?: unknown[]
enabled?: boolean
// forceMountSection?: boolean
// sectionMeta?: Record<string, unknown>
// orderSection?: OrderSectionInstruction
// orderCommands?: OrderCommandsInstruction
}
export const useRegisterCommandEffect = (
options: CommandOptions | CommandOptions[],
registerOptions?: RegisterOptions,
) => {
// TODO memo command via useMemo
// See https://github.com/supabase/supabase/blob/master/packages/ui-patterns/CommandMenu/api/hooks/commandsHooks.ts
useEffect(() => {
if (!Array.isArray(options)) {
return registerCommand(options)
}
const unsubscribes = options.map((option) => registerCommand(option))
return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe())
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- we want to run this effect only once
}, registerOptions?.deps ?? [])
}

View File

@ -0,0 +1,83 @@
import { assertType, expectTypeOf, test } from "vitest"
import { COMMAND_ID } from "../commands/id"
import { useRegisterFollowCommand } from "./use-register-command"
test("useRegisterFollowCommand types", () => {
assertType(
useRegisterFollowCommand({
id: COMMAND_ID.entry.openInBrowser,
label: "",
run: ({ entryId }) => {
expectTypeOf(entryId).toEqualTypeOf<string>()
},
}),
)
assertType(
useRegisterFollowCommand({
id: "unknown id",
label: "",
run: (...args) => {
expectTypeOf(args).toEqualTypeOf<[]>()
},
}),
)
assertType(
useRegisterFollowCommand([
{
id: COMMAND_ID.entry.star,
label: "",
run: ({ entryId }) => {
expectTypeOf(entryId).toEqualTypeOf<string>()
},
},
{
id: COMMAND_ID.entry.viewEntryContent,
label: "",
run: (...args) => {
expectTypeOf(args).toEqualTypeOf<[]>()
},
},
]),
)
assertType(
useRegisterFollowCommand([
{
id: "unknown id",
label: "",
run: (...args) => {
expectTypeOf(args).toEqualTypeOf<[]>()
},
},
]),
)
assertType(
useRegisterFollowCommand([
{
id: "unknown id",
label: "",
run: (...args) => {
expectTypeOf(args).toEqualTypeOf<[]>()
},
},
{
id: COMMAND_ID.entry.star,
label: "",
run: ({ entryId }) => {
expectTypeOf(entryId).toEqualTypeOf<string>()
},
},
{
id: COMMAND_ID.entry.viewEntryContent,
label: "",
run: (...args) => {
expectTypeOf(args).toEqualTypeOf<[]>()
},
},
]),
)
})

View File

@ -0,0 +1,66 @@
import { useEffect } from "react"
import { useTranslation } from "react-i18next"
import { registerCommand } from "../registry/registry"
import type { CommandOptions, FollowCommandId, FollowCommandMap } from "../types"
export type RegisterOptions = {
deps?: unknown[]
enabled?: boolean
// forceMountSection?: boolean
// sectionMeta?: Record<string, unknown>
// orderSection?: OrderSectionInstruction
// orderCommands?: OrderCommandsInstruction
}
export const useRegisterCommandEffect = (
options: CommandOptions | CommandOptions[],
registerOptions?: RegisterOptions,
) => {
const { t } = useTranslation()
useEffect(() => {
if (!Array.isArray(options)) {
return registerCommand(options)
}
const unsubscribes = options.map((option) => registerCommand(option))
return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe())
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- we want to run this effect only once
}, [t, ...(registerOptions?.deps ?? [])])
}
/**
* Register a follow command.
*/
export function useRegisterFollowCommand<T extends FollowCommandId>(
options: CommandOptions<{ id: T; fn: FollowCommandMap[T]["run"] }>,
registerOptions?: RegisterOptions,
): void
/**
* Register a unknown command.
*/
export function useRegisterFollowCommand<T extends string>(
options: CommandOptions<{ id: T; fn: () => void }>,
registerOptions?: RegisterOptions,
): void
/**
* Register multiple follow commands or unknown commands.
*/
export function useRegisterFollowCommand<T extends (FollowCommandId | string)[]>(
options: [
...{
[K in keyof T]: T[K] extends FollowCommandId
? CommandOptions<{ id: T[K]; fn: FollowCommandMap[T[K]]["run"] }>
: CommandOptions<{ id: T[K]; fn: () => void }>
},
],
registerOptions?: RegisterOptions,
): void
export function useRegisterFollowCommand(
options: CommandOptions | CommandOptions[],
registerOptions?: RegisterOptions,
) {
return useRegisterCommandEffect(options as CommandOptions | CommandOptions[], registerOptions)
}

View File

@ -1,7 +1,7 @@
import { assertType, expectTypeOf, test } from "vitest"
import { COMMAND_ID } from "../commands/id"
import { defineCommandArgsArray, defineFollowCommand, defineFollowCommandArgs } from "./command"
import { defineFollowCommand } from "./command"
test("defineFollowCommand types", () => {
assertType(
@ -68,71 +68,3 @@ test("defineFollowCommand with keyBinding types", () => {
}),
)
})
test("defineCommandArgs with keyBinding types", () => {
assertType(
defineFollowCommandArgs({
commandId: COMMAND_ID.entry.star,
args: [{ entryId: "1" }],
}),
)
assertType(
defineFollowCommandArgs({
commandId: COMMAND_ID.entry.star,
// @ts-expect-error - invalid args
args: [],
}),
)
})
test("defineCommandArgsArray with keyBinding types", () => {
assertType(
defineCommandArgsArray([
{
commandId: COMMAND_ID.entry.star,
args: [{ entryId: "1" }],
},
]),
)
assertType(
defineCommandArgsArray([
{
commandId: COMMAND_ID.entry.star,
// @ts-expect-error - invalid args
args: [],
},
]),
)
assertType(
defineCommandArgsArray([
{
commandId: COMMAND_ID.entry.star,
// @ts-expect-error - invalid args
args: [],
},
{
commandId: COMMAND_ID.entry.viewEntryContent,
args: [],
},
]),
)
assertType(
defineCommandArgsArray<{ test: boolean }>([
{
commandId: COMMAND_ID.entry.star,
args: [{ entryId: "1" }],
test: true,
},
{
commandId: COMMAND_ID.entry.viewEntryContent,
args: [],
// @ts-expect-error - invalid extra property
test: 1,
},
]),
)
})

View File

@ -1,4 +1,10 @@
import type { Command, CommandOptions, FollowCommand, FollowCommandId } from "../types"
import type {
Command,
CommandOptions,
FollowCommand,
FollowCommandId,
FollowCommandMap,
} from "../types"
export function createCommand<
T extends { id: string; fn: (...args: any[]) => unknown } = {
@ -32,33 +38,7 @@ export function createFollowCommand<T extends FollowCommand>(
}
export function defineFollowCommand<T extends FollowCommandId>(
options: CommandOptions<{ id: T; fn: Extract<FollowCommand, { id: T }>["run"] }>,
options: CommandOptions<{ id: T; fn: FollowCommandMap[T]["run"] }>,
) {
return options as CommandOptions
}
/**
* @deprecated
*/
export const defineFollowCommandArgs = <T extends FollowCommandId>(config: {
commandId: T
args: Parameters<Extract<FollowCommand, { id: T }>["run"]>
}) => config
/**
* @deprecated
*/
export const defineCommandArgsArray = <
Ext extends Record<string, unknown>,
T extends FollowCommandId[] = FollowCommandId[],
>(
config: [
...{
[K in keyof T]: {
commandId: T[K]
args: Parameters<Extract<FollowCommand, { id: T[K] }>["run"]>
// [key: string]: unknown
} & Ext
},
],
) => config