diff --git a/packages/parsers/package.json b/packages/parsers/package.json index d8387c51fb..ec7638ad22 100644 --- a/packages/parsers/package.json +++ b/packages/parsers/package.json @@ -20,9 +20,6 @@ "format:check": "prettier --check --ignore-unknown --ignore-path ../../shared/.prettierignore \"**\"", "organize-imports": "organize-imports-cli tsconfig.json", "depcheck": "depcheck", - "docs:dev": "NODE_OPTIONS='--inspect' next dev", - "docs:build": "next build", - "docs:start": "next start", "lint": "pnpm lint:eslint && pnpm lint:style" }, "dependencies": { diff --git a/packages/ui/app/src/atoms/playground.ts b/packages/ui/app/src/atoms/playground.ts index db376db27b..669be26f2d 100644 --- a/packages/ui/app/src/atoms/playground.ts +++ b/packages/ui/app/src/atoms/playground.ts @@ -7,7 +7,7 @@ import { import * as FernNavigation from "@fern-api/fdr-sdk/navigation"; import { useEventCallback } from "@fern-ui/react-commons"; import { WritableAtom, atom, useAtom, useAtomValue, useSetAtom } from "jotai"; -import { atomFamily, atomWithStorage, useAtomCallback } from "jotai/utils"; +import { RESET, atomFamily, atomWithStorage, useAtomCallback } from "jotai/utils"; import { Dispatch, SetStateAction, useEffect } from "react"; import { useCallbackOne } from "use-memo-one"; import { selectHref } from "../hooks/useHref"; @@ -184,17 +184,31 @@ export const PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM = atom( get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.bearer_token ?? "", }), - (_get, set, update: SetStateAction) => { - set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({ - ...prev, - bearerAuth: + (_get, set, update: SetStateAction | typeof RESET) => { + set(PLAYGROUND_AUTH_STATE_ATOM, ({ bearerAuth: prevBearerAuth, ...rest }) => { + const nextBearerAuth = typeof update === "function" - ? update(prev.bearerAuth ?? PLAYGROUND_AUTH_STATE_BEARER_TOKEN_INITIAL) - : update, - })); + ? update(prevBearerAuth ?? PLAYGROUND_AUTH_STATE_BEARER_TOKEN_INITIAL) + : update; + + if (nextBearerAuth === RESET) { + return rest; + } + + return { ...rest, bearerAuth: nextBearerAuth }; + }); }, ); +/** + * If an injected bearer token is provided, the input bearer token should be resettable if it's not empty. + */ +export const PLAYGROUND_AUTH_STATE_BEARER_TOKEN_IS_RESETTABLE_ATOM = atom((get) => { + const inputBearerAuth = get(PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM).token; + const injectedBearerAuth = get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.bearer_token; + return injectedBearerAuth != null && inputBearerAuth !== injectedBearerAuth; +}); + export const PLAYGROUND_AUTH_STATE_HEADER_ATOM = atom( (get) => ({ headers: pascalCaseHeaderKeys({ @@ -202,11 +216,17 @@ export const PLAYGROUND_AUTH_STATE_HEADER_ATOM = atom( get(FERN_USER_ATOM)?.playground?.initial_state?.headers), }), }), - (_get, set, update: SetStateAction) => { - set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({ - ...prev, - header: typeof update === "function" ? update(prev.header ?? PLAYGROUND_AUTH_STATE_HEADER_INITIAL) : update, - })); + (_get, set, update: SetStateAction | typeof RESET) => { + set(PLAYGROUND_AUTH_STATE_ATOM, ({ header: prevHeader, ...rest }) => { + const nextHeader = + typeof update === "function" ? update(prevHeader ?? PLAYGROUND_AUTH_STATE_HEADER_INITIAL) : update; + + if (nextHeader === RESET) { + return rest; + } + + return { ...rest, header: nextHeader }; + }); }, ); @@ -214,31 +234,88 @@ export const PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM = atom( (get) => ({ username: get(PLAYGROUND_AUTH_STATE_ATOM).basicAuth?.username ?? - get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic?.username ?? - "", + get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic?.username, password: get(PLAYGROUND_AUTH_STATE_ATOM).basicAuth?.password ?? - get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic?.password ?? - "", + get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic?.password, }), - (_get, set, update: SetStateAction) => { - set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({ - ...prev, - basicAuth: + (_get, set, update: SetStateAction> | typeof RESET) => { + set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => { + if (prev == null) { + return {}; + } + + const { basicAuth: prevBasicAuth, ...rest } = prev; + + const nextBasicAuth = typeof update === "function" - ? update(prev.basicAuth ?? PLAYGROUND_AUTH_STATE_BASIC_AUTH_INITIAL) - : update, - })); + ? update(prevBasicAuth ?? PLAYGROUND_AUTH_STATE_BASIC_AUTH_INITIAL) + : update; + + if (nextBasicAuth === RESET) { + return rest; + } + + return { ...rest, basicAuth: { username: "", password: "", ...prevBasicAuth, ...nextBasicAuth } }; + }); }, ); +export const PLAYGROUND_AUTH_STATE_BASIC_AUTH_USERNAME_ATOM = atom( + (get) => get(PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM).username, + (_get, set, update: SetStateAction | typeof RESET) => { + set(PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM, ({ username: prevUsername, ...rest }) => { + const nextUsername = typeof update === "function" ? update(prevUsername ?? "") : update; + + if (nextUsername === RESET) { + return rest; + } + + return { ...rest, username: nextUsername }; + }); + }, +); + +export const PLAYGROUND_AUTH_STATE_BASIC_AUTH_PASSWORD_ATOM = atom( + (get) => get(PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM).password, + (_get, set, update: SetStateAction | typeof RESET) => { + set(PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM, ({ password: prevPassword, ...rest }) => { + const nextPassword = typeof update === "function" ? update(prevPassword ?? "") : update; + + if (nextPassword === RESET) { + return rest; + } + + return { ...rest, password: nextPassword }; + }); + }, +); + +export const PLAYGROUND_AUTH_STATE_BASIC_AUTH_USERNAME_IS_RESETTABLE_ATOM = atom((get) => { + const inputBasicAuth = get(PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM); + const injectedBasicAuth = get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic; + return injectedBasicAuth != null && inputBasicAuth.username !== injectedBasicAuth.username; +}); + +export const PLAYGROUND_AUTH_STATE_BASIC_AUTH_PASSWORD_IS_RESETTABLE_ATOM = atom((get) => { + const inputBasicAuth = get(PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM); + const injectedBasicAuth = get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic; + return injectedBasicAuth != null && inputBasicAuth.password !== injectedBasicAuth.password; +}); + export const PLAYGROUND_AUTH_STATE_OAUTH_ATOM = atom( (get) => get(PLAYGROUND_AUTH_STATE_ATOM).oauth ?? PLAYGROUND_AUTH_STATE_OAUTH_INITIAL, - (_get, set, update: SetStateAction) => { - set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({ - ...prev, - oauth: typeof update === "function" ? update(prev.oauth ?? PLAYGROUND_AUTH_STATE_OAUTH_INITIAL) : update, - })); + (_get, set, update: SetStateAction | typeof RESET) => { + set(PLAYGROUND_AUTH_STATE_ATOM, ({ oauth: prevOauth, ...rest }) => { + const nextOauth = + typeof update === "function" ? update(prevOauth ?? PLAYGROUND_AUTH_STATE_OAUTH_INITIAL) : update; + + if (nextOauth === RESET) { + return rest; + } + + return { ...rest, oauth: nextOauth }; + }); }, ); diff --git a/packages/ui/app/src/playground/PasswordInputGroup.tsx b/packages/ui/app/src/playground/PasswordInputGroup.tsx index a8a54c2ba8..2e7f26c49d 100644 --- a/packages/ui/app/src/playground/PasswordInputGroup.tsx +++ b/packages/ui/app/src/playground/PasswordInputGroup.tsx @@ -1,17 +1,18 @@ import { FernButton, FernInput, FernInputProps } from "@fern-ui/components"; import { useBooleanState } from "@fern-ui/react-commons"; import { Eye, Lock } from "iconoir-react"; -import { FC } from "react"; +import { forwardRef } from "react"; -export const PasswordInputGroup: FC = ({ ref, ...props }) => { +export const PasswordInputGroup = forwardRef((props, forwardedRef) => { const showPassword = useBooleanState(false); return ( } {...props} type={showPassword.value ? "text" : "password"} rightElement={ - props.value != null && props.value.length > 0 ? ( + typeof props.value === "string" && props.value.length > 0 ? ( } @@ -25,4 +26,6 @@ export const PasswordInputGroup: FC = ({ ref, ...props }) => { } /> ); -}; +}); + +PasswordInputGroup.displayName = "PasswordInputGroup"; diff --git a/packages/ui/app/src/playground/auth/PlaygroundBasicAuthForm.tsx b/packages/ui/app/src/playground/auth/PlaygroundBasicAuthForm.tsx index c1fdbde28b..c12a37f94e 100644 --- a/packages/ui/app/src/playground/auth/PlaygroundBasicAuthForm.tsx +++ b/packages/ui/app/src/playground/auth/PlaygroundBasicAuthForm.tsx @@ -1,9 +1,15 @@ import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; import { FernInput } from "@fern-ui/components"; import { User } from "iconoir-react"; -import { useAtom } from "jotai/react"; -import { ReactElement, useCallback } from "react"; -import { PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM } from "../../atoms"; +import { useAtom, useAtomValue } from "jotai/react"; +import { RESET } from "jotai/utils"; +import { ReactElement } from "react"; +import { + PLAYGROUND_AUTH_STATE_BASIC_AUTH_PASSWORD_ATOM, + PLAYGROUND_AUTH_STATE_BASIC_AUTH_PASSWORD_IS_RESETTABLE_ATOM, + PLAYGROUND_AUTH_STATE_BASIC_AUTH_USERNAME_ATOM, + PLAYGROUND_AUTH_STATE_BASIC_AUTH_USERNAME_IS_RESETTABLE_ATOM, +} from "../../atoms"; import { PasswordInputGroup } from "../PasswordInputGroup"; export function PlaygroundBasicAuthForm({ @@ -13,15 +19,10 @@ export function PlaygroundBasicAuthForm({ basicAuth: APIV1Read.BasicAuth; disabled?: boolean; }): ReactElement { - const [value, setValue] = useAtom(PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM); - const handleChangeUsername = useCallback( - (newValue: string) => setValue((prev) => ({ ...prev, username: newValue })), - [setValue], - ); - const handleChangePassword = useCallback( - (newValue: string) => setValue((prev) => ({ ...prev, password: newValue })), - [setValue], - ); + const [username, setUsername] = useAtom(PLAYGROUND_AUTH_STATE_BASIC_AUTH_USERNAME_ATOM); + const [password, setPassword] = useAtom(PLAYGROUND_AUTH_STATE_BASIC_AUTH_PASSWORD_ATOM); + const isUsernameResettable = useAtomValue(PLAYGROUND_AUTH_STATE_BASIC_AUTH_USERNAME_IS_RESETTABLE_ATOM); + const isPasswordResettable = useAtomValue(PLAYGROUND_AUTH_STATE_BASIC_AUTH_PASSWORD_IS_RESETTABLE_ATOM); return ( <>
  • @@ -30,11 +31,13 @@ export function PlaygroundBasicAuthForm({
    } rightElement={{"string"}} disabled={disabled} + resettable={isUsernameResettable} + onClickReset={() => setUsername(RESET)} />
  • @@ -46,9 +49,11 @@ export function PlaygroundBasicAuthForm({
    setPassword(RESET)} />
    diff --git a/packages/ui/app/src/playground/auth/PlaygroundBearerAuthForm.tsx b/packages/ui/app/src/playground/auth/PlaygroundBearerAuthForm.tsx index f48976a74c..925fa5232f 100644 --- a/packages/ui/app/src/playground/auth/PlaygroundBearerAuthForm.tsx +++ b/packages/ui/app/src/playground/auth/PlaygroundBearerAuthForm.tsx @@ -1,7 +1,11 @@ import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; -import { useAtom } from "jotai/react"; -import { ReactElement, useCallback } from "react"; -import { PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM } from "../../atoms"; +import { useAtom, useAtomValue } from "jotai/react"; +import { RESET } from "jotai/utils"; +import { ReactElement } from "react"; +import { + PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM, + PLAYGROUND_AUTH_STATE_BEARER_TOKEN_IS_RESETTABLE_ATOM, +} from "../../atoms"; import { PasswordInputGroup } from "../PasswordInputGroup"; export function PlaygroundBearerAuthForm({ @@ -12,7 +16,7 @@ export function PlaygroundBearerAuthForm({ disabled?: boolean; }): ReactElement { const [value, setValue] = useAtom(PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM); - const handleChange = useCallback((newValue: string) => setValue({ token: newValue }), [setValue]); + const isBearerTokenResettable = useAtomValue(PLAYGROUND_AUTH_STATE_BEARER_TOKEN_IS_RESETTABLE_ATOM); return (
  • @@ -22,11 +26,13 @@ export function PlaygroundBearerAuthForm({
    setValue({ token: newValue })} value={value.token} autoComplete="off" data-1p-ignore="true" disabled={disabled} + resettable={isBearerTokenResettable} + onClickReset={() => setValue(RESET)} />
  • diff --git a/packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx b/packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx index 045d3fd930..3f0113088d 100644 --- a/packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx +++ b/packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx @@ -1,13 +1,49 @@ import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; import { unknownToString } from "@fern-api/ui-core-utils"; import { atom } from "jotai"; -import { useAtom } from "jotai/react"; +import { useAtom, useAtomValue } from "jotai/react"; +import { RESET } from "jotai/utils"; import type { ReactElement, SetStateAction } from "react"; import { useMemoOne } from "use-memo-one"; -import { PLAYGROUND_AUTH_STATE_HEADER_ATOM } from "../../atoms"; +import { FERN_USER_ATOM, PLAYGROUND_AUTH_STATE_HEADER_ATOM } from "../../atoms"; import { PasswordInputGroup } from "../PasswordInputGroup"; import { pascalCaseHeaderKey } from "../utils/header-key-case"; +function headerAtom(headerName: string) { + return atom( + (get) => get(PLAYGROUND_AUTH_STATE_HEADER_ATOM).headers[pascalCaseHeaderKey(headerName)], + (_get, set, change: SetStateAction | typeof RESET) => { + set(PLAYGROUND_AUTH_STATE_HEADER_ATOM, ({ headers }) => { + const nextHeaderValue = + typeof change === "function" ? change(headers[pascalCaseHeaderKey(headerName)] ?? "") : change; + if (nextHeaderValue === RESET) { + return { + // note: this will remove all undefined values from the object + headers: JSON.parse( + JSON.stringify({ ...headers, [pascalCaseHeaderKey(headerName)]: undefined }), + ), + }; + } + return { + headers: { + ...headers, + [pascalCaseHeaderKey(headerName)]: nextHeaderValue, + }, + }; + }); + }, + ); +} + +function isHeaderResettableAtom(headerName: string) { + return atom((get) => { + const inputHeader = get(PLAYGROUND_AUTH_STATE_HEADER_ATOM).headers[pascalCaseHeaderKey(headerName)]; + const injectedHeader = + get(FERN_USER_ATOM)?.playground?.initial_state?.headers?.[pascalCaseHeaderKey(headerName)]; + return injectedHeader != null && injectedHeader !== inputHeader; + }); +} + export function PlaygroundHeaderAuthForm({ header, disabled, @@ -15,26 +51,9 @@ export function PlaygroundHeaderAuthForm({ header: APIV1Read.HeaderAuth; disabled?: boolean; }): ReactElement { - const [value, setValue] = useAtom( - useMemoOne( - () => - atom( - (get) => - get(PLAYGROUND_AUTH_STATE_HEADER_ATOM).headers[pascalCaseHeaderKey(header.headerWireValue)], - (_get, set, change: SetStateAction) => { - set(PLAYGROUND_AUTH_STATE_HEADER_ATOM, ({ headers }) => ({ - headers: { - ...headers, - [pascalCaseHeaderKey(header.headerWireValue)]: - typeof change === "function" - ? change(headers[pascalCaseHeaderKey(header.headerWireValue)] ?? "") - : change, - }, - })); - }, - ), - [header.headerWireValue], - ), + const [value, setValue] = useAtom(useMemoOne(() => headerAtom(header.headerWireValue), [header.headerWireValue])); + const isResettable = useAtomValue( + useMemoOne(() => isHeaderResettableAtom(header.headerWireValue), [header.headerWireValue]), ); return ( @@ -51,6 +70,8 @@ export function PlaygroundHeaderAuthForm({ autoComplete="off" data-1p-ignore="true" disabled={disabled} + resettable={isResettable} + onClickReset={() => setValue(RESET)} /> diff --git a/packages/ui/app/src/playground/endpoint/PlaygroundEndpointForm.tsx b/packages/ui/app/src/playground/endpoint/PlaygroundEndpointForm.tsx index cf90c089e6..29c6cd09ac 100644 --- a/packages/ui/app/src/playground/endpoint/PlaygroundEndpointForm.tsx +++ b/packages/ui/app/src/playground/endpoint/PlaygroundEndpointForm.tsx @@ -1,11 +1,13 @@ import { PropertyKey, type EndpointContext } from "@fern-api/fdr-sdk/api-definition"; import { EMPTY_ARRAY, visitDiscriminatedUnion } from "@fern-api/ui-core-utils"; +import { useAtomValue } from "jotai"; import { Dispatch, FC, SetStateAction, useCallback, useMemo } from "react"; +import { FERN_USER_ATOM } from "../../atoms"; import { PlaygroundFileUploadForm } from "../form/PlaygroundFileUploadForm"; import { PlaygroundObjectForm } from "../form/PlaygroundObjectForm"; import { PlaygroundObjectPropertiesForm } from "../form/PlaygroundObjectPropertyForm"; import { PlaygroundEndpointRequestFormState, PlaygroundFormStateBody } from "../types"; -import { pascalCaseHeaderKey } from "../utils/header-key-case"; +import { pascalCaseHeaderKey, pascalCaseHeaderKeys } from "../utils/header-key-case"; import { PlaygroundEndpointAliasForm } from "./PlaygroundEndpointAliasForm"; import { PlaygroundEndpointFormSection } from "./PlaygroundEndpointFormSection"; import { PlaygroundEndpointMultipartForm } from "./PlaygroundEndpointMultipartForm"; @@ -23,6 +25,12 @@ export const PlaygroundEndpointForm: FC = ({ setFormState, ignoreHeaders, }) => { + const { + headers: initialHeaders, + query_parameters: initialQueryParameters, + path_parameters: initialPathParameters, + } = useAtomValue(FERN_USER_ATOM)?.playground?.initial_state ?? {}; + const setHeaders = useCallback( (value: ((old: unknown) => unknown) | unknown) => { setFormState((state) => ({ @@ -112,6 +120,7 @@ export const PlaygroundEndpointForm: FC = ({ extraProperties={undefined} onChange={setHeaders} value={formState?.headers} + defaultValue={pascalCaseHeaderKeys(initialHeaders)} types={types} /> @@ -125,6 +134,7 @@ export const PlaygroundEndpointForm: FC = ({ extraProperties={undefined} onChange={setPathParameters} value={formState?.pathParameters} + defaultValue={initialPathParameters} types={types} /> @@ -138,6 +148,7 @@ export const PlaygroundEndpointForm: FC = ({ extraProperties={undefined} onChange={setQueryParameters} value={formState?.queryParameters} + defaultValue={initialQueryParameters} types={types} /> diff --git a/packages/ui/app/src/playground/form/PlaygroundObjectForm.tsx b/packages/ui/app/src/playground/form/PlaygroundObjectForm.tsx index 9002b8823b..416b565e23 100644 --- a/packages/ui/app/src/playground/form/PlaygroundObjectForm.tsx +++ b/packages/ui/app/src/playground/form/PlaygroundObjectForm.tsx @@ -10,9 +10,17 @@ interface PlaygroundObjectFormProps { indent?: boolean; types: Record; disabled?: boolean; + defaultValue?: unknown; } -export function PlaygroundObjectForm({ id, shape, onChange, value, types }: PlaygroundObjectFormProps): ReactElement { +export function PlaygroundObjectForm({ + id, + shape, + onChange, + value, + types, + defaultValue, +}: PlaygroundObjectFormProps): ReactElement { const { properties, extraProperties } = useMemo(() => unwrapObjectType(shape, types), [shape, types]); return ( ); diff --git a/packages/ui/app/src/playground/form/PlaygroundObjectPropertyForm.tsx b/packages/ui/app/src/playground/form/PlaygroundObjectPropertyForm.tsx index 1e162572f2..0fc9e88665 100644 --- a/packages/ui/app/src/playground/form/PlaygroundObjectPropertyForm.tsx +++ b/packages/ui/app/src/playground/form/PlaygroundObjectPropertyForm.tsx @@ -5,6 +5,7 @@ import { TypeReference, unwrapReference, } from "@fern-api/fdr-sdk/api-definition"; +import { isPlainObject } from "@fern-api/ui-core-utils"; import { FernButton, FernDropdown } from "@fern-ui/components"; import { useBooleanState } from "@fern-ui/react-commons"; import cn from "clsx"; @@ -28,6 +29,7 @@ interface PlaygroundObjectPropertyFormProps { expandByDefault?: boolean; types: Record; disabled?: boolean; + defaultValue?: unknown; } export const PlaygroundObjectPropertyForm: FC = ({ @@ -38,6 +40,7 @@ export const PlaygroundObjectPropertyForm: FC expandByDefault = true, types, disabled, + defaultValue, }) => { const handleChange = useCallback( (newValue: unknown) => { @@ -75,6 +78,7 @@ export const PlaygroundObjectPropertyForm: FC onCloseStack={handleCloseStack} types={types} disabled={disabled} + defaultValue={defaultValue} /> ); }; @@ -85,13 +89,14 @@ interface PlaygroundObjectPropertiesFormProps { extraProperties: TypeReference | undefined; onChange: (value: unknown) => void; value: unknown; + defaultValue?: unknown; indent?: boolean; types: Record; disabled?: boolean; } export const PlaygroundObjectPropertiesForm = memo((props) => { - const { id, properties, onChange, value, indent = false, types, disabled, extraProperties } = props; + const { id, properties, onChange, value, indent = false, types, disabled, extraProperties, defaultValue } = props; const onChangeObjectProperty = useCallback( (key: string, newValue: unknown) => { @@ -198,6 +203,7 @@ export const PlaygroundObjectPropertiesForm = memo ); diff --git a/packages/ui/app/src/playground/form/PlaygroundTypeReferenceForm.tsx b/packages/ui/app/src/playground/form/PlaygroundTypeReferenceForm.tsx index 5640ddaf18..bc54476334 100644 --- a/packages/ui/app/src/playground/form/PlaygroundTypeReferenceForm.tsx +++ b/packages/ui/app/src/playground/form/PlaygroundTypeReferenceForm.tsx @@ -30,12 +30,13 @@ interface PlaygroundTypeReferenceFormProps { renderAsPanel?: boolean; types: Record; disabled?: boolean; + defaultValue?: unknown; indent?: boolean; } export const PlaygroundTypeReferenceForm = memo((props) => { const { hasVoiceIdPlaygroundForm } = useFeatureFlags(); - const { id, property, shape, onChange, value, types, disabled, indent = true } = props; + const { id, property, shape, onChange, value, types, disabled, indent = true, defaultValue } = props; const onRemove = useCallback(() => { onChange(undefined); }, [onChange]); @@ -50,6 +51,7 @@ export const PlaygroundTypeReferenceForm = memo ), @@ -67,6 +69,7 @@ export const PlaygroundTypeReferenceForm = memo ), @@ -79,6 +82,7 @@ export const PlaygroundTypeReferenceForm = memo ), @@ -87,6 +91,7 @@ export const PlaygroundTypeReferenceForm = memo ( {hasVoiceIdPlaygroundForm && property?.key === "voice_id" ? ( + // TODO: delete this:
    {/* */} - + {checked == null ? "undefined" : checked ? "true" : "false"} + */} +
    ); @@ -133,7 +145,8 @@ export const PlaygroundTypeReferenceForm = memo @@ -147,7 +160,8 @@ export const PlaygroundTypeReferenceForm = memo @@ -162,7 +176,8 @@ export const PlaygroundTypeReferenceForm = memo @@ -174,6 +189,8 @@ export const PlaygroundTypeReferenceForm = memo @@ -211,6 +232,8 @@ export const PlaygroundTypeReferenceForm = memo @@ -236,6 +261,8 @@ export const PlaygroundTypeReferenceForm = memo @@ -247,6 +274,8 @@ export const PlaygroundTypeReferenceForm = memo @@ -262,11 +291,13 @@ export const PlaygroundTypeReferenceForm = memo ), set: (set) => ( + {/* TODO: add default value */} ), @@ -279,6 +310,7 @@ export const PlaygroundTypeReferenceForm = memo ), @@ -304,6 +336,7 @@ export const PlaygroundTypeReferenceForm = memo ), diff --git a/packages/ui/components/package.json b/packages/ui/components/package.json index b14416a8ee..d79ae26fa4 100644 --- a/packages/ui/components/package.json +++ b/packages/ui/components/package.json @@ -38,24 +38,31 @@ "@emotion/is-prop-valid": "^1.2.2", "@fern-ui/react-commons": "workspace:*", "@radix-ui/colors": "^3.0.0", + "@radix-ui/primitive": "^1.1.0", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-collapsible": "^1.1.0", + "@radix-ui/react-compose-refs": "^1.1.0", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", "@storybook/client-api": "^7.6.17", + "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "copyfiles": "^2.4.1", + "es-toolkit": "^1.24.0", "iconoir-react": "^7.7.0", + "lucide-react": "^0.460.0", "moment": "^2.30.1", "react": "^18.2.0", "react-dom": "^18.2.0", "sonner": "^1.5.0", + "tailwind-merge": "^2.3.0", "ts-extras": "^0.11.0" }, "devDependencies": { diff --git a/packages/ui/components/src/FernButtonV2.tsx b/packages/ui/components/src/FernButtonV2.tsx new file mode 100644 index 0000000000..614a765515 --- /dev/null +++ b/packages/ui/components/src/FernButtonV2.tsx @@ -0,0 +1,54 @@ +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; +import { cn } from "./cn"; + +const buttonVariants = cva( + cn( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors hover:transition-none", + "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", + "disabled:pointer-events-none disabled:opacity-50", + "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + ), + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-[var(--grayscale-2)] hover:text-[var(--accent-12)]", + secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "text-[var(--grayscale-11)] hover:bg-[var(--accent-a3)] hover:text-[var(--accent-11)]", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + xs: "h-6 rounded-md px-2 text-xs", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "size-9", + iconSm: "size-7", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ; + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/packages/ui/components/src/FernInput.tsx b/packages/ui/components/src/FernInput.tsx index 790dc303f3..c7227aad4f 100644 --- a/packages/ui/components/src/FernInput.tsx +++ b/packages/ui/components/src/FernInput.tsx @@ -1,36 +1,135 @@ +import { composeEventHandlers } from "@radix-ui/primitive"; +import { composeRefs } from "@radix-ui/react-compose-refs"; import cn from "clsx"; -import { ComponentProps, forwardRef } from "react"; +import { isEqual } from "es-toolkit/predicate"; +import { Undo2 } from "lucide-react"; +import { ComponentPropsWithoutRef, forwardRef, useRef } from "react"; +import { Button } from "./FernButtonV2"; +import { FernTooltip, FernTooltipProvider } from "./FernTooltip"; -export interface FernInputProps extends ComponentProps<"input"> { +export interface FernInputProps extends Omit, "value" | "defaultValue"> { + value?: string; + defaultValue?: string; + + /** + * Additional classes to apply to the input element + */ inputClassName?: string; + /** + * Icon to render on the left side of the input + */ leftIcon?: React.ReactNode; + /** + * Element to render on the right side of the input + */ rightElement?: React.ReactNode; + /** + * Callback to call when the value changes + */ onValueChange?: (value: string) => void; - value?: string; + /** + * Whether the input should render a reset button if the default value is different from the current value + * @default false + */ + resettable?: boolean; + /** + * Callback to call when the reset button is clicked + */ + onClickReset?: () => void; } export const FernInput = forwardRef(function FernInput( - { className, inputClassName, value, onChange, onValueChange, leftIcon, rightElement, ...props }, - ref, + { className, inputClassName, onValueChange, leftIcon, rightElement, resettable, onClickReset, ...props }, + forwardedRef, ) { + const inputRef = useRef(null); + return (
    {leftIcon && {leftIcon}} { - if (props.maxLength != null && e.target.value.length > props.maxLength) { - return; - } + onChange={composeEventHandlers( + props.onChange, + (e) => { + if (props.maxLength != null && e.currentTarget.value.length > props.maxLength) { + return; + } - onChange?.(e); - onValueChange?.(e.target.value); - }} - {...props} + onValueChange?.(e.currentTarget.value); + }, + { checkForDefaultPrevented: true }, + )} + placeholder={props.placeholder ?? props.defaultValue} /> - {rightElement && {rightElement}} + { + if (props.defaultValue != null) { + onValueChange?.(props.defaultValue); + inputRef.current?.focus(); + } + }) + } + resettable={resettable} + > + {rightElement} +
    ); }); + +const FernInputResetButton = forwardRef< + HTMLButtonElement, + Omit, "variant" | "size" | "children"> +>(function FernInputResetButton({ onClick, ...props }, forwardedRef) { + return ( + + ); +}); + +function FernInputRightElement({ + children, + value, + defaultValue, + onReset, + resettable, +}: { + children?: React.ReactNode; + value?: string; + defaultValue?: string; + onReset: () => void; + resettable?: boolean; +}) { + if (resettable && defaultValue != null && !isEqual(value, defaultValue)) { + return ( + + +

    Reset to the default value:

    +

    + {defaultValue} +

    + + } + > + +
    +
    + ); + } + + if (!children) { + return null; + } + + return {children}; +} diff --git a/packages/ui/components/src/FernNumericInput.tsx b/packages/ui/components/src/FernNumericInput.tsx index 3a81b2df95..9116d6fee8 100644 --- a/packages/ui/components/src/FernNumericInput.tsx +++ b/packages/ui/components/src/FernNumericInput.tsx @@ -18,10 +18,11 @@ export interface FernNumericInputProps extends ComponentProps<"input"> { onValueChange?: (value: number) => void; value?: number; disallowFloat?: boolean; + rightElement?: React.ReactNode; } export const FernNumericInput = forwardRef(function FernInput( - { className, inputClassName, value, onChange, onValueChange, disallowFloat, ...props }, + { className, inputClassName, value, onChange, onValueChange, rightElement, disallowFloat, ...props }, ref, ) { const inputRef = useRef(null); @@ -119,6 +120,7 @@ export const FernNumericInput = forwardRef + {rightElement} {onValueChange && (