Skip to content

Commit

Permalink
feat: fill path
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <[email protected]>
  • Loading branch information
Innei committed Jun 26, 2024
1 parent 003cb18 commit 1149d24
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 32 deletions.
1 change: 1 addition & 0 deletions src/renderer/src/components/ui/modal/stacked/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export const ModalInternal: Component<{
</Wrapper>
)
}

return (
<Wrapper>
<Dialog.Root open onOpenChange={onClose}>
Expand Down
38 changes: 36 additions & 2 deletions src/renderer/src/lib/path-parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { compile, pathToRegexp } from "path-to-regexp"
import { describe, expect, test } from "vitest"

import { MissingOptionalParamError, MissingRequiredParamError, parseRegexpPathParams, regexpPathToPath, transformUriPath } from "./path-parser"
import { MissingOptionalParamError, MissingRequiredParamError, parseFullPathParams, parseRegexpPathParams, regexpPathToPath, transformUriPath } from "./path-parser"

describe("test `transformUriPath()`", () => {
test("normal path", () => {
Expand Down Expand Up @@ -168,7 +168,14 @@ describe("test `regexpPathToPath()`", () => {
repo: "follow",
labels: "rss",
}),
).throws(MissingOptionalParamError)
).toThrowError(MissingOptionalParamError)
})
test("path with many optional params, but when using the optional parameter(s) after the optional parameter(s), the previous optional parameter(s) is/are not filled in.", () => {
expect(
() => regexpPathToPath("/ranking/:rid?/:day?/:arc_type?/:disableEmbed?", {
day: "1",
}),
).toThrowError(MissingOptionalParamError)
})

test("missing required param", () => {
Expand All @@ -180,6 +187,15 @@ describe("test `regexpPathToPath()`", () => {
catchAll: "a/b/c",
})).toMatchInlineSnapshot(`"/a%2Fb%2Fc"`)
})

test("path with many optional params and all inputted", () => {
expect(regexpPathToPath("/issue/:user/:repo/:state?/:labels?", {
user: "rssnext",
repo: "follow",
state: "open",
labels: "rss",
})).toMatchInlineSnapshot(`"/issue/rssnext/follow/open/rss"`)
})
})

describe("test `parseRegexpPathParams()`", () => {
Expand Down Expand Up @@ -311,3 +327,21 @@ describe("test `parseRegexpPathParams()`", () => {
`)
})
})

describe("test `parseFullPathParams()`", () => {
test("case 1", () => {
expect(parseFullPathParams("/user/123344", "/user/:id")).toMatchInlineSnapshot(`
{
"id": "123344",
}
`)
})
test("case 2", () => {
expect(parseFullPathParams("/build/wangqiru/ttrss", "/build/:user/:name")).toMatchInlineSnapshot(`
{
"name": "ttrss",
"user": "wangqiru",
}
`)
})
})
21 changes: 18 additions & 3 deletions src/renderer/src/lib/path-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export function transformUriPath(uri: string): string {
}

export class MissingOptionalParamError extends Error {
constructor(param: string) {
super(`Missing optional param after optional param ${param}`)
constructor(public param: string) {
super(`You used the optional parameter ${param}, but there are other optional parameters before the optional parameter ${param} that you did not fill in.`)
}
}

Expand All @@ -49,6 +49,7 @@ export const regexpPathToPath = (
const paramsKeys = pathToRegexp(transformedPath).keys
const inputtedKeys = new Set(Object.keys(params))
let prevKeyIsOptional = false
let prevKeyIsInputted = false
for (const key of paramsKeys) {
if (key.name === CATCH_ALL_GROUP_KEY) {
params[CATCH_ALL_GROUP_KEY] = params.catchAll
Expand All @@ -64,11 +65,12 @@ export const regexpPathToPath = (
throw new Error("Required param after optional param")
}

if (prevKeyIsOptional && userInputted) {
if (prevKeyIsOptional && !prevKeyIsInputted && userInputted) {
throw new MissingOptionalParamError(key.name)
}

prevKeyIsOptional = isOptional
prevKeyIsInputted = userInputted
}
return compile(transformedPath, {
validate: false,
Expand Down Expand Up @@ -105,3 +107,16 @@ export const parseRegexpPathParams = (regexpPath: string) => {
length: array.length,
}
}
export const parseFullPathParams = (path: string, regexpPath: string) => {
// path: /user/123344
// regexpPath: /user/:id
// return {id: '123344'}
const transformedPath = transformUriPath(regexpPath)
const { keys } = pathToRegexp(transformedPath)
const result = pathToRegexp(transformedPath).exec(path)
if (!result) return {}
return keys.reduce((acc, key, index) => {
acc[key.name] = result[index + 1]
return acc
}, {} as Record<string, string>)
}
95 changes: 69 additions & 26 deletions src/renderer/src/modules/discover/recommendation-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import { Markdown } from "@renderer/components/ui/markdown"
import { useModalStack } from "@renderer/components/ui/modal/stacked/hooks"
import { FeedViewType } from "@renderer/lib/enum"
import {
MissingOptionalParamError,
parseFullPathParams,
parseRegexpPathParams,
regexpPathToPath,
} from "@renderer/lib/path-parser"
import type { FC } from "react"
import { useCallback, useMemo } from "react"
import type { UseFormReturn } from "react-hook-form"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"

import { FeedForm } from "./feed-form"
Expand Down Expand Up @@ -47,6 +50,13 @@ const DiscoverFeedForm = ({
}) => {
const keys = parseRegexpPathParams(route.path)

const formPlaceholder = useMemo<Record<string, string>>(() => {
if (!route.example) return {}
return parseFullPathParams(
route.example.replace(`/${routePrefix}`, ""),
route.path,
)
}, [route.example, route.path, routePrefix])
const dynamicFormSchema = useMemo(
() =>
z.object({
Expand All @@ -62,25 +72,45 @@ const DiscoverFeedForm = ({
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(dynamicFormSchema),
defaultValues: {},
mode: "all",
}) as UseFormReturn<any>

const { present } = useModalStack()
const onSubmit = useCallback((data) => {
present({
title: "Add follow",
content: ({ dismiss }) => {
const url = `rsshub://${routePrefix}${regexpPathToPath(route.path, data)}`
return (
<FeedForm
asWidget
url={url}
defaultView={FeedViewType.Articles}
onSuccess={dismiss}
/>
)
},
})
}, [present, route.path, routePrefix])
const onSubmit = useCallback(
(data) => {
try {
// Delete empty string values
const nextData = { ...data }
for (const key in nextData) {
if (nextData[key] === "") {
delete nextData[key]
}
}

const fillRegexpPath = regexpPathToPath(route.path, nextData)
const url = `rsshub://${routePrefix}${fillRegexpPath}`
present({
title: "Add follow",
content: ({ dismiss }) => (
<FeedForm
asWidget
url={url}
defaultView={FeedViewType.Articles}
onSuccess={dismiss}
/>
),
})
} catch (err: unknown) {
if (err instanceof MissingOptionalParamError) {
toast.error(err.message)
const idx = keys.array.findIndex((item) => item.name === err.param)

form.setFocus(keys.array[idx === 0 ? 0 : idx - 1].name)
}
}
},
[form, keys.array, present, route.path, routePrefix],
)

return (
<Form {...form}>
Expand All @@ -96,7 +126,10 @@ const DiscoverFeedForm = ({
<sup className="ml-1 align-sub text-red-500">*</sup>
)}
</FormLabel>
<Input {...form.register(keyItem.name)} />
<Input
{...form.register(keyItem.name)}
placeholder={formPlaceholder[keyItem.name]}
/>
<p className="text-xs text-theme-foreground/50">
{route.parameters[keyItem.name]}
</p>
Expand All @@ -105,10 +138,16 @@ const DiscoverFeedForm = ({

<PreviewUrl
watch={form.watch}
path={`rsshub://${routePrefix}${route.path}`}
path={route.path}
routePrefix={`rsshub://${routePrefix}`}
/>
<div className="mt-4 flex justify-end">
<StyledButton type="submit">Preview</StyledButton>
<StyledButton
disabled={!form.formState.isValid}
type="submit"
>
Preview
</StyledButton>
</div>
</form>
</Form>
Expand All @@ -118,19 +157,23 @@ const DiscoverFeedForm = ({
const PreviewUrl: FC<{
watch: UseFormReturn<any>["watch"]
path: string
}> = ({ watch, path }) => {
routePrefix: string
}> = ({ watch, path, routePrefix }) => {
const data = watch()

const fullPath = useMemo(() => {
try {
const url = new URL(path)
const { protocol, pathname } = url

return `${protocol}/${regexpPathToPath(pathname, data)}`
} catch {
return regexpPathToPath(path, data)
} catch (err: unknown) {
console.info((err as Error).message)
return path
}
}, [path, data])

return <pre className="text-xs text-theme-foreground/30">{fullPath}</pre>
return (
<pre className="text-xs text-theme-foreground/30">
{routePrefix}
{fullPath}
</pre>
)
}
1 change: 0 additions & 1 deletion src/renderer/src/modules/discover/recommendations-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export const RecommendationCard: FC<RecommendationCardProps> = memo(({ data, rou
/>
),
title: `${data.name} - ${data.routes[route].name}`,
clickOutsideToDismiss: true,
})
}}
>
Expand Down

0 comments on commit 1149d24

Please sign in to comment.