From 11ac63f232a3aa1237304fc54a8baae2d7cfc464 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Sun, 12 Jan 2025 21:05:25 +0000 Subject: [PATCH 01/14] Rough in a skeleton for a shadcn version of autocomponents --- .../cypress/component/auto/AutoButton.cy.tsx | 33 +- packages/react/cypress/support/auto.tsx | 16 +- packages/react/cypress/support/component.tsx | 7 +- packages/react/cypress/support/cypress.css | 3 + packages/react/package.json | 22 +- packages/react/postcss.config.js | 6 + .../shadcn-defaults/components/Button.tsx | 42 ++ .../auto/shadcn-defaults/components/Toast.tsx | 109 ++++ .../shadcn-defaults/components/Toaster.tsx | 26 + .../auto/shadcn-defaults/hooks/useToast.tsx | 186 ++++++ .../react/spec/auto/shadcn-defaults/index.tsx | 10 + .../react/spec/auto/shadcn-defaults/utils.ts | 6 + .../src/auto/shadcn/ShadcnAutoButton.tsx | 41 ++ packages/react/src/auto/shadcn/elements.tsx | 23 + packages/react/src/auto/shadcn/index.ts | 16 + packages/react/tailwind.config.js | 8 + pnpm-lock.yaml | 578 +++++++++++++++++- 17 files changed, 1087 insertions(+), 45 deletions(-) create mode 100644 packages/react/cypress/support/cypress.css create mode 100644 packages/react/postcss.config.js create mode 100644 packages/react/spec/auto/shadcn-defaults/components/Button.tsx create mode 100644 packages/react/spec/auto/shadcn-defaults/components/Toast.tsx create mode 100644 packages/react/spec/auto/shadcn-defaults/components/Toaster.tsx create mode 100644 packages/react/spec/auto/shadcn-defaults/hooks/useToast.tsx create mode 100644 packages/react/spec/auto/shadcn-defaults/index.tsx create mode 100644 packages/react/spec/auto/shadcn-defaults/utils.ts create mode 100644 packages/react/src/auto/shadcn/ShadcnAutoButton.tsx create mode 100644 packages/react/src/auto/shadcn/elements.tsx create mode 100644 packages/react/src/auto/shadcn/index.ts create mode 100644 packages/react/tailwind.config.js diff --git a/packages/react/cypress/component/auto/AutoButton.cy.tsx b/packages/react/cypress/component/auto/AutoButton.cy.tsx index 619c6a871..cefb8f73c 100644 --- a/packages/react/cypress/component/auto/AutoButton.cy.tsx +++ b/packages/react/cypress/component/auto/AutoButton.cy.tsx @@ -9,7 +9,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp }); it("shows a button, runs a create action with no variables, and reports success", () => { - cy.mountWithWrapper(, wrapper); + cy.mountWithWrapper(, wrapper); cy.contains("Create Widget"); cy.intercept("POST", `${api.connection.options.endpoint}?operation=createWidget`, { @@ -23,7 +23,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp }, }).as("createWidget"); - cy.get("button").click(); + cy.get("#auto").click(); cy.wait("@createWidget"); cy.get("@createWidget").its("request.body.variables").should("deep.equal", { widget: {} }); @@ -32,7 +32,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp }); it("shows a button, runs a create action with variables, and reports success", () => { - cy.mountWithWrapper(, wrapper); + cy.mountWithWrapper(, wrapper); cy.contains("Create Widget"); cy.intercept("POST", `${api.connection.options.endpoint}?operation=createWidget`, { @@ -46,7 +46,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp }, }).as("createWidget"); - cy.get("button").click(); + cy.get("#auto").click(); cy.wait("@createWidget"); cy.get("@createWidget") @@ -59,14 +59,14 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp }); it("shows a button, runs a create action with no variables, and reports an error if the network call fails", () => { - cy.mountWithWrapper(, wrapper); + cy.mountWithWrapper(, wrapper); cy.contains("Create Widget"); cy.intercept("POST", `${api.connection.options.endpoint}?operation=createWidget`, { forceNetworkError: true, }).as("createWidget"); - cy.get("button").click(); + cy.get("#auto").click(); cy.wait("@createWidget"); @@ -74,7 +74,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp }); it("shows a button, runs an update action with variables, and reports success", () => { - cy.mountWithWrapper(, wrapper); + cy.mountWithWrapper(, wrapper); cy.contains("Update Widget"); cy.intercept("POST", `${api.connection.options.endpoint}?operation=updateWidget`, { @@ -88,7 +88,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp }, }).as("updateWidget"); - cy.get("button").click(); + cy.get("#auto").click(); cy.wait("@updateWidget"); cy.get("@updateWidget") @@ -102,7 +102,12 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp }); it("allows overriding the label", () => { - cy.mountWithWrapper(Whizbang the flimflam, wrapper); + cy.mountWithWrapper( + + Whizbang the flimflam + , + wrapper + ); cy.contains("Whizbang the flimflam"); }); @@ -110,6 +115,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp let onSuccessCalled = false; cy.mountWithWrapper( { @@ -131,7 +137,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp }, }).as("updateWidget"); - cy.get("button").click(); + cy.get("#auto").click(); cy.wait("@updateWidget").then(() => { expect(onSuccessCalled).to.be.true; @@ -142,6 +148,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp let onErrorCalled = false; cy.mountWithWrapper( { @@ -157,7 +164,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp forceNetworkError: true, }).as("updateWidget"); - cy.get("button").click(); + cy.get("#auto").click(); cy.wait("@updateWidget").then(() => { expect(onErrorCalled).to.be.true; @@ -165,7 +172,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp }); it("shows a button, runs an global action with no variables, and reports success", () => { - cy.mountWithWrapper(, wrapper); + cy.mountWithWrapper(, wrapper); cy.contains("Flip all"); cy.intercept("POST", `${api.connection.options.endpoint}?operation=flipAll`, { @@ -176,7 +183,7 @@ describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapp }, }).as("flipAll"); - cy.get("button").click(); + cy.get("#auto").click(); cy.wait("@flipAll"); cy.get("@flipAll").its("request.body.variables").should("deep.equal", {}); diff --git a/packages/react/cypress/support/auto.tsx b/packages/react/cypress/support/auto.tsx index 8d75ba298..d3e49bef2 100644 --- a/packages/react/cypress/support/auto.tsx +++ b/packages/react/cypress/support/auto.tsx @@ -3,8 +3,10 @@ import "@shopify/polaris/build/esm/styles.css"; import translations from "@shopify/polaris/locales/en.json"; import type { ComponentType, ReactNode } from "react"; import React from "react"; +import { Toaster, elements } from "../../spec/auto/shadcn-defaults/index.js"; import type { AutoAdapter } from "../../src/auto/index.js"; import * as PolarisAdapter from "../../src/auto/polaris/index.js"; +import { makeAutocomponents } from "../../src/auto/shadcn/index.js"; import { FormProvider, useForm } from "../../src/useActionForm.js"; interface AutoSuiteConfig { @@ -23,7 +25,19 @@ export const PolarisWrapper = ({ children }: { children: ReactNode }) => ( ); -const suites: AutoSuiteConfig[] = [{ name: "Polaris", adapter: PolarisAdapter as any, wrapper: PolarisWrapper }]; +const ShadCNAdapter = makeAutocomponents(elements); + +export const ShadcnWrapper = ({ children }: { children: ReactNode }) => ( + <> + + {children} + +); + +const suites: AutoSuiteConfig[] = [ + { name: "Polaris", adapter: PolarisAdapter as any, wrapper: PolarisWrapper }, + { name: "Shadcn", adapter: ShadCNAdapter as any, wrapper: ShadcnWrapper }, +]; export const adapters = [PolarisAdapter]; export const describeForEachAutoAdapter = (suiteName: string, suite: (config: AutoSuiteConfig) => void) => { diff --git a/packages/react/cypress/support/component.tsx b/packages/react/cypress/support/component.tsx index 924c170ec..2a427db9c 100644 --- a/packages/react/cypress/support/component.tsx +++ b/packages/react/cypress/support/component.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import React from "react"; import { Provider } from "../../src/index.js"; import "./commands.js"; +import "./cypress.css"; import { mount } from "cypress/react18"; import { api } from "./api.js"; @@ -57,7 +58,11 @@ before(() => { message.innerHTML = msg; document.body.appendChild(message); setTimeout(() => { - document.body.removeChild(message); + try { + document.body.removeChild(message); + } catch (e) { + // don't worry if the element has already been removed or changed parents + } }, 3000); }, }, diff --git a/packages/react/cypress/support/cypress.css b/packages/react/cypress/support/cypress.css new file mode 100644 index 000000000..b5c61c956 --- /dev/null +++ b/packages/react/cypress/support/cypress.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/packages/react/package.json b/packages/react/package.json index 617000929..7bbd9df8b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -21,6 +21,11 @@ "import": "./dist/esm/auto/polaris/index.js", "require": "./dist/cjs/auto/polaris/index.js", "default": "./dist/esm/auto/polaris/index.js" + }, + "./auto/shadcn": { + "import": "./dist/esm/auto/shadcn/index.js", + "require": "./dist/cjs/auto/shadcn/index.js", + "default": "./dist/esm/auto/shadcn/index.js" } }, "source": "src/index.ts", @@ -30,7 +35,7 @@ "typecheck": "tsc --noEmit", "clean": "rimraf dist/ auto/", "build": "pnpm clean && pnpm setup-build && tsc -b tsconfig.cjs.json tsconfig.esm.json && pnpm copy", - "setup-build": "mkdir -p dist/cjs dist/esm auto/polaris && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json && echo '{\"main\": \"../../dist/cjs/auto/polaris/index.js\"}' > auto/polaris/package.json && echo '{\"type\": \"module\"}' > dist/esm/package.json", + "setup-build": "mkdir -p dist/cjs dist/esm auto/polaris auto/shadcn && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json && echo '{\"main\": \"../../dist/cjs/auto/polaris/index.js\"}' > auto/polaris/package.json && echo '{\"main\": \"../../dist/cjs/auto/shadcn/index.js\"}' > auto/shadcn/package.json && echo '{\"type\": \"module\"}' > dist/esm/package.json", "copy": "copyfiles -u 1 src/**/*.css dist/esm && copyfiles -u 1 src/**/*.css dist/cjs", "watch": "tsc -b tsconfig.esm.json --watch --preserveWatchOutput", "gql-gen": "graphql-codegen", @@ -69,6 +74,7 @@ "@pollyjs/adapter-xhr": "^6.0.6", "@pollyjs/core": "^6.0.6", "@pollyjs/persister-fs": "^6.0.6", + "@radix-ui/react-toast": "^1.2.4", "@shopify/polaris": "^12.0.0", "@shopify/polaris-icons": "^8.1.0", "@storybook/addon-essentials": "^8.1.6", @@ -91,23 +97,31 @@ "@types/setup-polly-jest": "^0.5.5", "@types/tmp": "^0.2.6", "@urql/core": "^4.0.10", + "autoprefixer": "^10.4.20", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "conditional-type-checks": "^1.0.6", "copyfiles": "^2.4.1", "cypress": "^13.13.0", "cypress-each": "^1.13.3", + "date-fns": "^2.30.0", + "date-fns-tz": "^2.0.0", "execa": "^5.1.1", "graphql": "^16.8.1", "graphql-ws": "^5.13.1", "libnpmpack": "^7.0.4", "lodash-es": "^4.17.21", + "lucide-react": "^0.471.0", + "postcss": "^8.4.49", "react": "^18.2.0", "react-dom": "^18.2.0", "setup-polly-jest": "^0.11.0", "storybook": "^8.1.6", + "tailwind-merge": "^2.6.0", + "tailwindcss": "^3.4.17", + "tailwindcss-animate": "^1.0.7", "tmp": "^0.2.3", - "wonka": "^6.3.2", - "date-fns": "^2.30.0", - "date-fns-tz": "^2.0.0" + "wonka": "^6.3.2" }, "peerDependencies": { "@mdxeditor/editor": "^3.8.0", diff --git a/packages/react/postcss.config.js b/packages/react/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/packages/react/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/packages/react/spec/auto/shadcn-defaults/components/Button.tsx b/packages/react/spec/auto/shadcn-defaults/components/Button.tsx new file mode 100644 index 000000000..b468328f3 --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/components/Button.tsx @@ -0,0 +1,42 @@ +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; +import { cn } from "../utils.js"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors 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-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + 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/react/spec/auto/shadcn-defaults/components/Toast.tsx b/packages/react/spec/auto/shadcn-defaults/components/Toast.tsx new file mode 100644 index 000000000..79177ecd9 --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/components/Toast.tsx @@ -0,0 +1,109 @@ +"use client"; +import * as ToastPrimitives from "@radix-ui/react-toast"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; +import React from "react"; +import { cn } from "../utils.js"; + +const ToastProvider = ToastPrimitives.Provider; + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & VariantProps +>(({ className, variant, ...props }, ref) => { + return ; +}); +Toast.displayName = ToastPrimitives.Root.displayName; + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ); +ToastDescription.displayName = ToastPrimitives.Description.displayName; + +type ToastProps = React.ComponentPropsWithoutRef; + +type ToastActionElement = React.ReactElement; + +export { + Toast, + ToastAction, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, + type ToastActionElement, + type ToastProps, +}; diff --git a/packages/react/spec/auto/shadcn-defaults/components/Toaster.tsx b/packages/react/spec/auto/shadcn-defaults/components/Toaster.tsx new file mode 100644 index 000000000..123032e11 --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/components/Toaster.tsx @@ -0,0 +1,26 @@ +"use client"; +import React from "react"; +import { useToast } from "../hooks/useToast.js"; +import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "./Toast.js"; + +export function Toaster() { + const { toasts } = useToast(); + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && {description}} +
+ {action} + +
+ ); + })} + +
+ ); +} diff --git a/packages/react/spec/auto/shadcn-defaults/hooks/useToast.tsx b/packages/react/spec/auto/shadcn-defaults/hooks/useToast.tsx new file mode 100644 index 000000000..f949d45fc --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/hooks/useToast.tsx @@ -0,0 +1,186 @@ +import type { ReactNode } from "react"; +import { useEffect, useState } from "react"; +import type { ToastActionElement, ToastProps } from "../components/Toast.js"; + +const TOAST_LIMIT = 1; +const TOAST_REMOVE_DELAY = 1000000; + +type ToasterToast = ToastProps & { + id: string; + title?: ReactNode; + description?: ReactNode; + action?: ToastActionElement; +}; + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const; + +let count = 0; + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); +} + +type ActionType = typeof actionTypes; + +type Action = + | { + type: ActionType["ADD_TOAST"]; + toast: ToasterToast; + } + | { + type: ActionType["UPDATE_TOAST"]; + toast: Partial; + } + | { + type: ActionType["DISMISS_TOAST"]; + toastId?: ToasterToast["id"]; + } + | { + type: ActionType["REMOVE_TOAST"]; + toastId?: ToasterToast["id"]; + }; + +interface State { + toasts: ToasterToast[]; +} + +const toastTimeouts = new Map>(); + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return; + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId); + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }); + }, TOAST_REMOVE_DELAY); + + toastTimeouts.set(toastId, timeout); +}; + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + }; + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)), + }; + + case "DISMISS_TOAST": { + const { toastId } = action; + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId); + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id); + }); + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + }; + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + }; + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + }; + } +}; + +const listeners: Array<(state: State) => void> = []; + +let memoryState: State = { toasts: [] }; + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action); + listeners.forEach((listener) => { + listener(memoryState); + }); +} + +type Toast = Omit; + +function toast({ ...props }: Toast) { + const id = genId(); + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }); + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss(); + }, + }, + }); + + return { + id: id, + dismiss, + update, + }; +} + +function useToast() { + const [state, setState] = useState(memoryState); + + useEffect(() => { + listeners.push(setState); + return () => { + const index = listeners.indexOf(setState); + if (index > -1) { + listeners.splice(index, 1); + } + }; + }, [state]); + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + }; +} + +export { toast, useToast }; diff --git a/packages/react/spec/auto/shadcn-defaults/index.tsx b/packages/react/spec/auto/shadcn-defaults/index.tsx new file mode 100644 index 000000000..6692d748f --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/index.tsx @@ -0,0 +1,10 @@ +import type { ShadcnElements } from "../../../src/auto/shadcn/elements.js"; +import { Button } from "./components/Button.js"; +import { toast } from "./hooks/useToast.js"; + +export const elements: ShadcnElements = { + Button, + toast, +}; + +export { Toaster } from "./components/Toaster.js"; diff --git a/packages/react/spec/auto/shadcn-defaults/utils.ts b/packages/react/spec/auto/shadcn-defaults/utils.ts new file mode 100644 index 000000000..a5ef19350 --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/packages/react/src/auto/shadcn/ShadcnAutoButton.tsx b/packages/react/src/auto/shadcn/ShadcnAutoButton.tsx new file mode 100644 index 000000000..0082469ff --- /dev/null +++ b/packages/react/src/auto/shadcn/ShadcnAutoButton.tsx @@ -0,0 +1,41 @@ +import type { ActionFunction, GlobalActionFunction } from "@gadgetinc/api-client-core"; +import type { ComponentProps } from "react"; +import React from "react"; +import type { OptionsType } from "../../utils.js"; +import type { AutoButtonProps } from "../hooks/useAutoButtonController.js"; +import { useAutoButtonController } from "../hooks/useAutoButtonController.js"; +import type { ShadcnElements } from "./elements.js"; + +/** + * Render a button that invokes an action when clicked, and shows a toast notification when the action succeeds or encounters an error by default. + */ +export const makeAutoButton = + ({ Button, toast }: Elements) => + < + GivenOptions extends OptionsType, + SchemaT, + ActionFunc extends ActionFunction | GlobalActionFunction + >( + props: AutoButtonProps & ComponentProps + ) => { + const { fetching, isDestructive, run, label, buttonProps } = useAutoButtonController({ + onSuccess: (_result) => { + toast({ + title: `${label} succeeded.`, + }); + }, + onError: (error, _result) => { + toast({ + title: `${label} encountered an error: ${error.message}.`, + variant: "destructive", + }); + }, + ...props, + }); + + return ( + + ); + }; diff --git a/packages/react/src/auto/shadcn/elements.tsx b/packages/react/src/auto/shadcn/elements.tsx new file mode 100644 index 000000000..212061a2e --- /dev/null +++ b/packages/react/src/auto/shadcn/elements.tsx @@ -0,0 +1,23 @@ +import type React from "react"; + +/** The props that a button component injected into autocomponent's shadcn must support */ +export interface ButtonProps extends React.ButtonHTMLAttributes { + asChild?: boolean; + variant?: any | null; +} + +/** One toast for showing via the toasting system */ +export type ToasterToast = { + className?: string; + title?: any; + description?: any; + action?: any; + variant?: any; +}; + +export interface ShadcnElements { + /** The Button component from shadcn */ + Button: React.ComponentType; + /** The toast imperative function from shadcn */ + toast: (props: ToasterToast) => void; +} diff --git a/packages/react/src/auto/shadcn/index.ts b/packages/react/src/auto/shadcn/index.ts new file mode 100644 index 000000000..0b536c947 --- /dev/null +++ b/packages/react/src/auto/shadcn/index.ts @@ -0,0 +1,16 @@ +import type { ShadcnElements } from "./elements.js"; +import { makeAutoButton } from "./ShadcnAutoButton.js"; +export * from "./elements.js"; + +/** + * Build the Autocomponents library for your shadcn presentation components. + * Autocomponents will render these given components, so they need to take the same base props that mainline shadcn components do. + */ +export const makeAutocomponents = (elements: ShadcnElements) => { + return { + AutoButton: makeAutoButton(elements), + //TODO: + // AutoTable: makeAutoTable(elements), + // AutoForm: makeAutoForm(elements), + }; +}; diff --git a/packages/react/tailwind.config.js b/packages/react/tailwind.config.js new file mode 100644 index 000000000..431e70157 --- /dev/null +++ b/packages/react/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./cypress/support/component-index.html", "./spec/auto/shadcn-defaults/**/*.{ts,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 137969f33..fb5a8d4bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,6 +302,9 @@ importers: '@pollyjs/persister-fs': specifier: ^6.0.6 version: 6.0.6 + '@radix-ui/react-toast': + specifier: ^1.2.4 + version: 1.2.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@shopify/polaris': specifier: ^12.0.0 version: 12.27.0(react-dom@18.2.0)(react@18.2.0) @@ -368,6 +371,15 @@ importers: '@urql/core': specifier: ^4.0.10 version: 4.0.10(graphql@16.8.1) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.49) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 conditional-type-checks: specifier: ^1.0.6 version: 1.0.6 @@ -401,6 +413,12 @@ importers: lodash-es: specifier: ^4.17.21 version: 4.17.21 + lucide-react: + specifier: ^0.471.0 + version: 0.471.0(react@18.2.0) + postcss: + specifier: ^8.4.49 + version: 8.4.49 react: specifier: ^18.2.0 version: 18.2.0 @@ -413,6 +431,15 @@ importers: storybook: specifier: ^8.1.6 version: 8.1.6(react-dom@18.2.0)(react@18.2.0) + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.17) tmp: specifier: ^0.2.3 version: 0.2.3 @@ -593,6 +620,11 @@ packages: resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} dev: true + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: true + /@ampproject/remapping@2.2.0: resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} engines: {node: '>=6.0.0'} @@ -800,7 +832,7 @@ packages: '@babel/compat-data': 7.19.0 '@babel/core': 7.19.0 '@babel/helper-validator-option': 7.18.6 - browserslist: 4.20.3 + browserslist: 4.23.1 semver: 6.3.1 dev: true @@ -877,7 +909,7 @@ packages: '@babel/helper-plugin-utils': 7.24.7 debug: 4.3.4(supports-color@8.1.1) lodash.debounce: 4.0.8 - resolve: 1.22.2 + resolve: 1.22.8 transitivePeerDependencies: - supports-color dev: true @@ -5801,6 +5833,10 @@ packages: resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} dev: true + /@radix-ui/primitive@1.1.1: + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + dev: true + /@radix-ui/react-arrow@1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} peerDependencies: @@ -5844,6 +5880,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@radix-ui/react-collection@1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.79)(react@18.2.0): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -5871,6 +5930,19 @@ packages: react: 18.2.0 dev: true + /@radix-ui/react-compose-refs@1.1.1(@types/react@18.2.79)(react@18.2.0): + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.79 + react: 18.2.0 + dev: true + /@radix-ui/react-context@1.0.1(@types/react@18.2.79)(react@18.2.0): resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} peerDependencies: @@ -5898,6 +5970,19 @@ packages: react: 18.2.0 dev: true + /@radix-ui/react-context@1.1.1(@types/react@18.2.79)(react@18.2.0): + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.79 + react: 18.2.0 + dev: true + /@radix-ui/react-dialog@1.0.5(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} peerDependencies: @@ -5994,6 +6079,30 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.79)(react@18.2.0): resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: @@ -6208,6 +6317,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@radix-ui/react-portal@1.1.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} peerDependencies: @@ -6251,6 +6381,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@radix-ui/react-presence@1.1.2(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: @@ -6292,6 +6443,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@radix-ui/react-primitive@2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} peerDependencies: @@ -6409,6 +6580,51 @@ packages: react: 18.2.0 dev: true + /@radix-ui/react-slot@1.1.1(@types/react@18.2.79)(react@18.2.0): + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@types/react': 18.2.79 + react: 18.2.0 + dev: true + + /@radix-ui/react-toast@1.2.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Sch9idFJHJTMH9YNpxxESqABcAFweJG4tKv+0zo0m5XBvUSL8FM5xKcJLFLXononpePs8IclyX1KieL5SDUNgA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@radix-ui/react-toggle-group@1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==} peerDependencies: @@ -6687,6 +6903,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@radix-ui/rect@1.1.0: resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} dev: true @@ -9039,6 +9275,10 @@ packages: resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==} dev: true + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + /anymatch@3.1.2: resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} engines: {node: '>= 8'} @@ -9055,6 +9295,10 @@ packages: resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} dev: true + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -9229,6 +9473,22 @@ packages: engines: {node: '>=8'} dev: true + /autoprefixer@10.4.20(postcss@8.4.49): + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.24.4 + caniuse-lite: 1.0.30001692 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.1 + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + dev: true + /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} @@ -9561,6 +9821,13 @@ packages: fill-range: 7.0.1 dev: true + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + dev: true + /browser-assert@1.2.1: resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} dev: true @@ -9575,18 +9842,6 @@ packages: pako: 0.2.9 dev: true - /browserslist@4.20.3: - resolution: {integrity: sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001343 - electron-to-chromium: 1.4.140 - escalade: 3.1.2 - node-releases: 2.0.5 - picocolors: 1.0.0 - dev: true - /browserslist@4.23.1: resolution: {integrity: sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -9598,6 +9853,17 @@ packages: update-browserslist-db: 1.0.16(browserslist@4.23.1) dev: true + /browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001692 + electron-to-chromium: 1.5.80 + node-releases: 2.0.19 + update-browserslist-db: 1.1.2(browserslist@4.24.4) + dev: true + /bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: @@ -9702,6 +9968,11 @@ packages: tslib: 2.6.2 dev: true + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} @@ -9712,14 +9983,14 @@ packages: engines: {node: '>=10'} dev: true - /caniuse-lite@1.0.30001343: - resolution: {integrity: sha512-8KeCrAtPMabo/XW14B+R9sZYoClx1n0b+WYgwDKZPtWR3TcdvWzdSy7mPyFEmR5WU1St9v1PW6sdO5dkFOEzfA==} - dev: true - /caniuse-lite@1.0.30001632: resolution: {integrity: sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==} dev: true + /caniuse-lite@1.0.30001692: + resolution: {integrity: sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==} + dev: true + /capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} dependencies: @@ -9911,6 +10182,12 @@ packages: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} dev: true + /class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + dependencies: + clsx: 2.1.1 + dev: true + /classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} dev: true @@ -9988,6 +10265,11 @@ packages: engines: {node: '>=0.8'} dev: true + /clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + dev: true + /cm6-theme-basic-light@0.2.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.28.4)(@lezer/highlight@1.2.0): resolution: {integrity: sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==} peerDependencies: @@ -10067,6 +10349,11 @@ packages: engines: {node: '>=14'} dev: true + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + /commander@6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} @@ -10689,6 +10976,10 @@ packages: dequal: 2.0.3 dev: true + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -10713,6 +11004,10 @@ packages: path-type: 4.0.0 dev: true + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + /doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -10820,14 +11115,14 @@ packages: jake: 10.9.1 dev: true - /electron-to-chromium@1.4.140: - resolution: {integrity: sha512-NLz5va823QfJBYOO/hLV4AfU4Crmkl/6Hl2pH3qdJcmi0ySZ3YTWHxOlDm3uJOFBEPy3pIhu8gKQo6prQTWKKA==} - dev: true - /electron-to-chromium@1.4.798: resolution: {integrity: sha512-by9J2CiM9KPGj9qfp5U4FcPSbXJG7FNzqnYaY4WLzX+v2PHieVGmnsA4dxfpGE3QEC7JofpPZmn7Vn1B9NR2+Q==} dev: true + /electron-to-chromium@1.5.80: + resolution: {integrity: sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw==} + dev: true + /emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -11104,6 +11399,11 @@ packages: engines: {node: '>=6'} dev: true + /escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + dev: true + /escape-carriage@1.3.1: resolution: {integrity: sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==} dev: true @@ -11707,7 +12007,7 @@ packages: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.5 + micromatch: 4.0.8 dev: true /fast-json-stable-stringify@2.1.0: @@ -11826,6 +12126,13 @@ packages: to-regex-range: 5.0.1 dev: true + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + /finalhandler@1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} engines: {node: '>= 0.8'} @@ -11963,6 +12270,10 @@ packages: engines: {node: '>= 0.6'} dev: true + /fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + dev: true + /fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -14131,6 +14442,11 @@ packages: - supports-color dev: true + /lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + dev: true + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true @@ -14313,6 +14629,14 @@ packages: yallist: 3.1.1 dev: true + /lucide-react@0.471.0(react@18.2.0): + resolution: {integrity: sha512-3L0OOJClsKDETJGK7nABqW8ftaVmUjWzluzPpw/6dGdI1bOmzsLsCjZpAEpg24Xs/U7xdYveQU+CBkHxWy7MrA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + dependencies: + react: 18.2.0 + dev: true + /lz-string@1.4.4: resolution: {integrity: sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==} hasBin: true @@ -14956,6 +15280,14 @@ packages: picomatch: 2.3.1 dev: true + /micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + dev: true + /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -15132,11 +15464,25 @@ packages: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} dev: true + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + /nanoid@3.3.6: resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true @@ -15258,8 +15604,8 @@ packages: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true - /node-releases@2.0.5: - resolution: {integrity: sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==} + /node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} dev: true /noms@0.0.0: @@ -15307,6 +15653,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + /npm-bundled@3.0.1: resolution: {integrity: sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -15424,6 +15775,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true @@ -15906,9 +16262,13 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true /picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} dev: true /picomatch@2.3.1: @@ -15985,6 +16345,55 @@ packages: engines: {node: '>= 0.4'} dev: true + /postcss-import@15.1.0(postcss@8.4.49): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + dev: true + + /postcss-js@4.0.1(postcss@8.4.49): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.49 + dev: true + + /postcss-load-config@4.0.2(postcss@8.4.49): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.1.3 + postcss: 8.4.49 + yaml: 2.4.5 + dev: true + + /postcss-nested@6.2.0(postcss@8.4.49): + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 + dev: true + /postcss-selector-parser@6.1.1: resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==} engines: {node: '>=4'} @@ -15993,14 +16402,35 @@ packages: util-deprecate: 1.0.2 dev: true + /postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + /postcss@8.4.27: resolution: {integrity: sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.6 - picocolors: 1.0.0 + picocolors: 1.0.1 source-map-js: 1.0.2 + /postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + dev: true + /preferred-pm@3.1.4: resolution: {integrity: sha512-lEHd+yEm22jXdCphDrkvIJQU66EuLojPPtvZkpKIkiD+l0DMThF/niqZKJSoU8Vl7iuvtmzyMhir9LdVy5WMnA==} engines: {node: '>=10'} @@ -16492,6 +16922,12 @@ packages: dependencies: loose-envify: 1.4.0 + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + /read-cmd-shim@4.0.0: resolution: {integrity: sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -17218,6 +17654,11 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + dev: true + /source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} dependencies: @@ -17550,6 +17991,20 @@ packages: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} dev: true + /sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: true + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -17594,6 +18049,49 @@ packages: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: true + /tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} + dev: true + + /tailwindcss-animate@1.0.7(tailwindcss@3.4.17): + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + dependencies: + tailwindcss: 3.4.17 + dev: true + + /tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.6 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-import: 15.1.0(postcss@8.4.49) + postcss-js: 4.0.1(postcss@8.4.49) + postcss-load-config: 4.0.2(postcss@8.4.49) + postcss-nested: 6.2.0(postcss@8.4.49) + postcss-selector-parser: 6.1.2 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + dev: true + /tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} dependencies: @@ -17672,6 +18170,19 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + /throttleit@1.0.1: resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} dev: true @@ -17785,6 +18296,10 @@ packages: resolution: {integrity: sha512-F8m9NOF6ZhdOClDVdlM8gj3fDCav4ZIFSs/EI3ksQbAAXVSCN/Jh5OCJDDZWBuBy9psFc6jULGDlPwjMYMhJDw==} dev: true + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + /ts-log@2.2.5: resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==} dev: true @@ -18132,6 +18647,17 @@ packages: picocolors: 1.0.1 dev: true + /update-browserslist-db@1.1.2(browserslist@4.24.4): + resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 + dev: true + /upper-case-first@2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} dependencies: From 369dc20fdda3d0c5b0948ae038ac80962ae5415e Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Fri, 17 Jan 2025 15:56:52 -0500 Subject: [PATCH 02/14] WIP Initial Shadcn Implementation --- packages/react/cypress/support/auto.tsx | 11 +- .../auto/shadcn-defaults/components/Alert.tsx | 59 ++++++++ .../auto/shadcn-defaults/components/Form.tsx | 21 +++ .../auto/shadcn-defaults/components/Input.tsx | 23 +++ .../auto/shadcn-defaults/components/Label.tsx | 25 ++++ .../shadcn-defaults/components/Skeleton.tsx | 32 +++++ .../react/spec/auto/shadcn-defaults/index.tsx | 13 ++ .../src/auto/shadcn/ShadcnAutoButton.tsx | 2 +- .../react/src/auto/shadcn/ShadcnAutoForm.tsx | 136 ++++++++++++++++++ packages/react/src/auto/shadcn/elements.tsx | 77 ++++++++-- packages/react/src/auto/shadcn/index.ts | 3 +- .../auto/shadcn/inputs/ShadcnAutoInput.tsx | 28 ++++ .../shadcn/inputs/ShadcnAutoNumberInput.tsx | 50 +++++++ .../shadcn/inputs/ShadcnAutoTextInput.tsx | 43 ++++++ .../auto/shadcn/submit/ShadcnAutoSubmit.tsx | 26 ++++ .../submit/ShadcnSubmitResultBanner.tsx | 60 ++++++++ 16 files changed, 593 insertions(+), 16 deletions(-) create mode 100644 packages/react/spec/auto/shadcn-defaults/components/Alert.tsx create mode 100644 packages/react/spec/auto/shadcn-defaults/components/Form.tsx create mode 100644 packages/react/spec/auto/shadcn-defaults/components/Input.tsx create mode 100644 packages/react/spec/auto/shadcn-defaults/components/Label.tsx create mode 100644 packages/react/spec/auto/shadcn-defaults/components/Skeleton.tsx create mode 100644 packages/react/src/auto/shadcn/ShadcnAutoForm.tsx create mode 100644 packages/react/src/auto/shadcn/inputs/ShadcnAutoInput.tsx create mode 100644 packages/react/src/auto/shadcn/inputs/ShadcnAutoNumberInput.tsx create mode 100644 packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx create mode 100644 packages/react/src/auto/shadcn/submit/ShadcnAutoSubmit.tsx create mode 100644 packages/react/src/auto/shadcn/submit/ShadcnSubmitResultBanner.tsx diff --git a/packages/react/cypress/support/auto.tsx b/packages/react/cypress/support/auto.tsx index d3e49bef2..2b374b33f 100644 --- a/packages/react/cypress/support/auto.tsx +++ b/packages/react/cypress/support/auto.tsx @@ -25,17 +25,20 @@ export const PolarisWrapper = ({ children }: { children: ReactNode }) => ( ); -const ShadCNAdapter = makeAutocomponents(elements); + +const ShadCNAdapter = makeAutocomponents({ ...elements }); export const ShadcnWrapper = ({ children }: { children: ReactNode }) => ( <> - - {children} + + + {children} + ); const suites: AutoSuiteConfig[] = [ - { name: "Polaris", adapter: PolarisAdapter as any, wrapper: PolarisWrapper }, +// { name: "Polaris", adapter: PolarisAdapter as any, wrapper: PolarisWrapper }, { name: "Shadcn", adapter: ShadCNAdapter as any, wrapper: ShadcnWrapper }, ]; diff --git a/packages/react/spec/auto/shadcn-defaults/components/Alert.tsx b/packages/react/spec/auto/shadcn-defaults/components/Alert.tsx new file mode 100644 index 000000000..ee07bf1d0 --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/components/Alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "../utils.js"; + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/packages/react/spec/auto/shadcn-defaults/components/Form.tsx b/packages/react/spec/auto/shadcn-defaults/components/Form.tsx new file mode 100644 index 000000000..410d27fda --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/components/Form.tsx @@ -0,0 +1,21 @@ +import { cn } from "../utils.js"; +import * as React from "react"; + +const Form = React.forwardRef< + HTMLFormElement, + React.FormHTMLAttributes +>(({ className, ...props }, ref) => { + return ( +
+ ); +}); +Form.displayName = "Form"; + +export { Form }; \ No newline at end of file diff --git a/packages/react/spec/auto/shadcn-defaults/components/Input.tsx b/packages/react/spec/auto/shadcn-defaults/components/Input.tsx new file mode 100644 index 000000000..1e1c2a1a6 --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/components/Input.tsx @@ -0,0 +1,23 @@ +import * as React from "react" + +import { cn } from "../utils.js"; + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/packages/react/spec/auto/shadcn-defaults/components/Label.tsx b/packages/react/spec/auto/shadcn-defaults/components/Label.tsx new file mode 100644 index 000000000..9b456c795 --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/components/Label.tsx @@ -0,0 +1,25 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "../utils.js"; + + + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } \ No newline at end of file diff --git a/packages/react/spec/auto/shadcn-defaults/components/Skeleton.tsx b/packages/react/spec/auto/shadcn-defaults/components/Skeleton.tsx new file mode 100644 index 000000000..f5b7ed91c --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/components/Skeleton.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "../utils.js"; + + +const skeletonVariants = cva("animate-pulse rounded-md bg-primary/10", { + variants: { + size: { + sm: "h-4 w-16", + md: "h-6 w-32", + lg: "h-8 w-64", + }, + shape: { + rectangle: "rounded-md", + circle: "rounded-full", + }, + }, + defaultVariants: { + size: "md", + shape: "rectangle", + }, +}); + +export interface SkeletonProps extends React.HTMLAttributes, VariantProps {} + +const Skeleton = React.forwardRef(({ className, size, shape, ...props }, ref) => { + return
; +}); + +Skeleton.displayName = "Skeleton"; + +export { Skeleton, skeletonVariants }; \ No newline at end of file diff --git a/packages/react/spec/auto/shadcn-defaults/index.tsx b/packages/react/spec/auto/shadcn-defaults/index.tsx index 6692d748f..c6184fd85 100644 --- a/packages/react/spec/auto/shadcn-defaults/index.tsx +++ b/packages/react/spec/auto/shadcn-defaults/index.tsx @@ -1,10 +1,23 @@ import type { ShadcnElements } from "../../../src/auto/shadcn/elements.js"; +import { Alert, AlertTitle, AlertDescription } from "./components/Alert.js"; import { Button } from "./components/Button.js"; +import { Label } from "./components/Label.js"; +import { Form } from "./components/Form.js"; +import { Input } from "./components/Input.js"; +import { Skeleton } from "./components/Skeleton.js"; import { toast } from "./hooks/useToast.js"; + export const elements: ShadcnElements = { Button, + Skeleton, toast, + Form, + Input, + Alert, + AlertTitle, + AlertDescription, + Label, }; export { Toaster } from "./components/Toaster.js"; diff --git a/packages/react/src/auto/shadcn/ShadcnAutoButton.tsx b/packages/react/src/auto/shadcn/ShadcnAutoButton.tsx index 0082469ff..e19584e1d 100644 --- a/packages/react/src/auto/shadcn/ShadcnAutoButton.tsx +++ b/packages/react/src/auto/shadcn/ShadcnAutoButton.tsx @@ -34,7 +34,7 @@ export const makeAutoButton = }); return ( - ); diff --git a/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx b/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx new file mode 100644 index 000000000..83db38d38 --- /dev/null +++ b/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx @@ -0,0 +1,136 @@ +import type { ActionFunction } from "@gadgetinc/api-client-core"; +import React, { ComponentProps } from "react"; +import { FormProvider } from "../../useActionForm.js"; +import { humanizeCamelCase, type OptionsType } from "../../utils.js"; +import type { AutoFormProps } from "../AutoForm.js"; +import { useAutoForm } from "../AutoForm.js"; +import { validateAutoFormProps } from "../AutoFormActionValidators.js"; +import { AutoFormMetadataContext } from "../AutoFormContext.js"; +import type { FormProps, ShadcnElements } from "./elements.js"; +import { makeSubmitResultBanner } from "./submit/ShadcnSubmitResultBanner.js"; +import { makeShadcnAutoInput } from "./inputs/ShadcnAutoInput.js"; +import { makeShadcnAutoSubmit } from "./submit/ShadcnAutoSubmit.js"; + +/** + * Renders a form for an action on a model automatically using Shadcn + */ +export const makeAutoForm = + ({ Form, Input, Button, Alert, Skeleton, AlertTitle, AlertDescription, Label }: Elements) => + < + GivenOptions extends OptionsType, + SchemaT, + ActionFunc extends ActionFunction + >( + props: AutoFormProps & ComponentProps + ) => { + const { action, findBy } = props as AutoFormProps & + Omit, "action"> & { findBy: any }; + + validateAutoFormProps(props); + + // Component key to force re-render when the action or findBy changes + const componentKey = `${action.modelApiIdentifier ?? ""}.${action.operationName}.${JSON.stringify(findBy)}`; + + const ShadcnAutoInput = makeShadcnAutoInput({ Input, Label }); + + return ( + & Omit, "action"> & { findBy: any })} + elements={{ Form, Input, Button, Alert, Skeleton, AlertTitle, AlertDescription, ShadcnAutoInput }} + /> + ); + } + + +const ShadcnAutoFormComponent = < + GivenOptions extends OptionsType, + SchemaT, + ActionFunc extends ActionFunction +>( + props: AutoFormProps & { + elements: ShadcnElements; + } & ComponentProps +) => { + const { + record: _record, + action, + findBy, + ...rest + } = props as AutoFormProps & Omit, "action"> & { findBy: any }; + const { Form, Button, Alert, Skeleton, AlertTitle, AlertDescription, ShadcnAutoInput } = props.elements; + + const { metadata, fetchingMetadata, metadataError, fields, submit, formError, isSubmitting, isSubmitSuccessful, originalFormMethods } = + useAutoForm(props); + + const { ShadcnSubmitSuccessfulBanner, ShadcnSubmitErrorBanner } = makeSubmitResultBanner({ Alert, AlertTitle, AlertDescription }); + const ShadcnAutoSubmit = makeShadcnAutoSubmit({ Button }); + + + const autoFormMetadataContext: AutoFormMetadataContext = { + findBy, + submit, + metadata, + submitResult: { + isSuccessful: isSubmitSuccessful, + error: formError ?? metadataError, + isSubmitting, + }, + model: { + apiIdentifier: action.modelApiIdentifier, + namespace: action.namespace, + }, + options: { + include: props.include, + exclude: props.exclude, + }, + }; + + const formTitle = props.title === undefined ? humanizeCamelCase(action.operationName) : props.title; + + + if (props.successContent && isSubmitSuccessful) { + return props.successContent; + } + + if (fetchingMetadata) { + return ( + + + + ); + } + + const formContent = props.children ?? ( + <> + {formTitle && ( +

{formTitle}

+ )} + {!props.onSuccess && } + {!props.onFailure && } + {!metadataError && ( + <> + {fields.map(({ metadata }) => ( + + ))} + + {props.submitLabel ?? "Submit"} + + + )} + + ); + + return ( + + +
+ {formContent} +
+
+
+ ); +} \ No newline at end of file diff --git a/packages/react/src/auto/shadcn/elements.tsx b/packages/react/src/auto/shadcn/elements.tsx index 212061a2e..5b7bb0156 100644 --- a/packages/react/src/auto/shadcn/elements.tsx +++ b/packages/react/src/auto/shadcn/elements.tsx @@ -1,23 +1,80 @@ import type React from "react"; +import type { PropsWithChildren } from "react"; + + +/** The props that a component injected into autocomponent's shadcn must support */ +export interface BaseProps extends PropsWithChildren { + variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"; + className?: string; +} /** The props that a button component injected into autocomponent's shadcn must support */ -export interface ButtonProps extends React.ButtonHTMLAttributes { +export interface ButtonProps + extends BaseProps, + React.ButtonHTMLAttributes { asChild?: boolean; - variant?: any | null; } +/** The props that a skeleton component injected into autocomponent's shadcn must support */ +export interface SkeletonProps extends Pick { +} + +/** The props that an alert component injected into autocomponent's shadcn must support */ +export interface AlertProps extends Omit { + variant?: "default" | "destructive"; +} + +/** The props that an alert title component injected into autocomponent's shadcn must support */ +export interface AlertTitleProps extends Pick { +} + +/** The props that an alert description component injected into autocomponent's shadcn must support */ +export interface AlertDescriptionProps extends Pick { +} + +/** The props that a form component injected into autocomponent's shadcn must support */ +export interface FormProps extends Pick { +} + +/** The props that an input component injected into autocomponent's shadcn must support */ +export interface InputProps extends Pick, React.InputHTMLAttributes { +} + +/** The props that a label component injected into autocomponent's shadcn must support */ +export interface LabelProps extends React.LabelHTMLAttributes { +} + + + /** One toast for showing via the toasting system */ -export type ToasterToast = { - className?: string; - title?: any; - description?: any; - action?: any; - variant?: any; -}; +export interface ToasterToast extends BaseProps { + title?: React.ReactNode; + description?: React.ReactNode; + action?: React.ReactNode; +} + + export interface ShadcnElements { + /** The Label component from shadcn */ + Label: React.ComponentType; /** The Button component from shadcn */ Button: React.ComponentType; + /** The Skeleton component from shadcn */ + Skeleton: React.ComponentType; + /** The Alert component from shadcn */ + Alert: React.ComponentType; + + /** The Form component from shadcn */ + Form: React.ComponentType; + + /** The Input component from shadcn */ + Input: React.ComponentType; + + /** The AlertTitle component from shadcn */ + AlertTitle: React.ComponentType; + /** The AlertDescription component from shadcn */ + AlertDescription: React.ComponentType; /** The toast imperative function from shadcn */ toast: (props: ToasterToast) => void; -} + } diff --git a/packages/react/src/auto/shadcn/index.ts b/packages/react/src/auto/shadcn/index.ts index 0b536c947..cb6ca9fcb 100644 --- a/packages/react/src/auto/shadcn/index.ts +++ b/packages/react/src/auto/shadcn/index.ts @@ -1,5 +1,6 @@ import type { ShadcnElements } from "./elements.js"; import { makeAutoButton } from "./ShadcnAutoButton.js"; +import { makeAutoForm } from "./ShadcnAutoForm.js"; export * from "./elements.js"; /** @@ -11,6 +12,6 @@ export const makeAutocomponents = (elements: ShadcnElements) => { AutoButton: makeAutoButton(elements), //TODO: // AutoTable: makeAutoTable(elements), - // AutoForm: makeAutoForm(elements), + AutoForm: makeAutoForm(elements), }; }; diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoInput.tsx new file mode 100644 index 000000000..aca0b37d0 --- /dev/null +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoInput.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { FieldType } from "../../../metadata.js"; +import { autoInput } from "../../AutoInput.js"; +import { useFieldMetadata } from "../../hooks/useFieldMetadata.js"; +import type { ShadcnElements } from "../elements.js"; +import { makeShadcnAutoTextInput } from "./ShadcnAutoTextInput.js"; +import { makeShadcnAutoNumberInput } from "./ShadcnAutoNumberInput.js"; + +export const makeShadcnAutoInput = ({ Input, Label }: Pick) => + autoInput((props: { field: string; label?: string }) => { + const { metadata } = useFieldMetadata(props.field); + const config = metadata.configuration; + + switch (config.fieldType) { + case FieldType.String: + case FieldType.Email: + case FieldType.Color: + case FieldType.Url: { + return makeShadcnAutoTextInput({ Input, Label })(props); + } + case FieldType.Number: { + return makeShadcnAutoNumberInput({ Input, Label })(props); + } + default: + //throw new Error(`Unsupported field type for Shadcn AutoForm: ${metadata.fieldType}`); + return makeShadcnAutoTextInput({ Input, Label })(props); + } + }); \ No newline at end of file diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoNumberInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoNumberInput.tsx new file mode 100644 index 000000000..c1a03d3f8 --- /dev/null +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoNumberInput.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import type { Control } from "../../../useActionForm.js"; +import { autoInput } from "../../AutoInput.js"; +import { useStringInputController } from "../../hooks/useStringInputController.js"; +import type { ShadcnElements } from "../elements.js"; +import { makeShadcnAutoTextInput } from "./ShadcnAutoTextInput.js"; + + +export const makeShadcnAutoNumberInput = ({ Input, Label }: Pick) => { + const TextInput = makeShadcnAutoTextInput({ Input, Label }); + + return autoInput((props: { + field: string; + control?: Control; + className?: string; + }) => { + const { field, control } = props; + const { metadata, value } = useStringInputController({ field, control }); + + const step = + metadata.configuration.__typename === "GadgetNumberConfig" && + metadata.configuration.decimals && + metadata.configuration.decimals > 0 + ? getStepFromNumberOfDecimals(metadata.configuration.decimals) + : value + ? getStepFromNumberOfDecimals(countNumberOfDecimals(`${value}`)) + : 1; + + return ( + + ); + }); +}; + +// TODO: move to a shared location + const getStepFromNumberOfDecimals = (numberOfDecimals: number) => + numberOfDecimals === 0 ? 1 : Number(`0.${"0".repeat(numberOfDecimals - 1)}1`); + + const countNumberOfDecimals = (value: string) => { + if (value.includes("e")) { + // +e scientific notation for large numbers does not get decimal steps + return 0; + } + const [, decimals] = value.split("."); + return decimals?.length ?? 0; + }; \ No newline at end of file diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx new file mode 100644 index 000000000..6f8c4c50e --- /dev/null +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import type { InputHTMLAttributes } from "react"; +import type { Control } from "../../../useActionForm.js"; +import { getPropsWithoutRef } from "../../../utils.js"; +import { autoInput } from "../../AutoInput.js"; +import { useStringInputController } from "../../hooks/useStringInputController.js"; +import type { ShadcnElements } from "../elements.js"; + +// should we move this to a shared location? +import { cn } from "../../../../spec/auto/shadcn-defaults/utils.js"; + +export const makeShadcnAutoTextInput = ({ Input, Label }: Pick) => + autoInput( + (props: { + field: string; // The field API identifier + control?: Control; + } & Partial>) => { + const { field, control } = props; + const stringInputController = useStringInputController({ field, control }); + const id = `${field}-input`; + + const label = stringInputController.label || stringInputController.metadata.name; + + + return ( +
+ + + {stringInputController.errorMessage && ( +

{stringInputController.errorMessage}

+ )} +
+ ); + } + ); \ No newline at end of file diff --git a/packages/react/src/auto/shadcn/submit/ShadcnAutoSubmit.tsx b/packages/react/src/auto/shadcn/submit/ShadcnAutoSubmit.tsx new file mode 100644 index 000000000..5031d5801 --- /dev/null +++ b/packages/react/src/auto/shadcn/submit/ShadcnAutoSubmit.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import type { ReactNode } from "react"; +import { useAutoFormMetadata } from "../../AutoFormContext.js"; +import type { ShadcnElements } from "../elements.js"; + +export const makeShadcnAutoSubmit = ({ Button }: Pick) => { + + return function ShadcnAutoSubmit(props: { + children?: ReactNode; + isSubmitting?: boolean; + className?: string; + }) { + const { submitResult } = useAutoFormMetadata(); + const isSubmitting = props.isSubmitting ?? submitResult.isSubmitting; + + return ( + + ); + }; +}; \ No newline at end of file diff --git a/packages/react/src/auto/shadcn/submit/ShadcnSubmitResultBanner.tsx b/packages/react/src/auto/shadcn/submit/ShadcnSubmitResultBanner.tsx new file mode 100644 index 000000000..70e66b835 --- /dev/null +++ b/packages/react/src/auto/shadcn/submit/ShadcnSubmitResultBanner.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import type { ShadcnElements } from "../elements.js"; +import { useResultBannerController } from "../../hooks/useResultBannerController.js"; + +export const makeSubmitResultBanner = >({ Alert, AlertTitle, AlertDescription }: Elements) => { + + if (!Alert || !AlertTitle || !AlertDescription) { + throw new Error("Alert components are required"); + } + + const ShadcnSubmitSuccessfulBanner = (props: any) => { + const { show, successful, title, hide } = useResultBannerController(); + + if (!show || !successful) { + return null; + } + + return ( + + Success + + {title || "Operation completed successfully"} + + + + ); + }; + + const ShadcnSubmitErrorBanner = (props: any) => { + const { show, successful, hide, title} = useResultBannerController(); + + if (!show || successful) { + return null; + } + return ( + + Error + + {title || "An error occurred"} + + + + ); + }; + + const ShadcnSubmitResultBanner = (props: { successBannerProps?: any; errorBannerProps?: any }) => { + return ( + <> + + + + ); + }; + + return { + ShadcnSubmitResultBanner, + ShadcnSubmitSuccessfulBanner, + ShadcnSubmitErrorBanner, + }; +}; From 6923baf9b997c3f0994e9027235aa36f28e4ccba Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Fri, 17 Jan 2025 15:57:46 -0500 Subject: [PATCH 03/14] Add React label --- packages/react/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/package.json b/packages/react/package.json index 7bbd9df8b..88482a263 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -74,6 +74,7 @@ "@pollyjs/adapter-xhr": "^6.0.6", "@pollyjs/core": "^6.0.6", "@pollyjs/persister-fs": "^6.0.6", + "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-toast": "^1.2.4", "@shopify/polaris": "^12.0.0", "@shopify/polaris-icons": "^8.1.0", From 9cfdf1cf7f273df94470fd0af71b533118c8ce4b Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Tue, 21 Jan 2025 15:58:25 -0500 Subject: [PATCH 04/14] Shadcn initial components --- .../component/auto/form/AutoForm.cy.tsx | 34 ++-- .../AutoFormDynamicFormInputChanges.cy.tsx | 14 +- .../auto/form/AutoFormGlobalActions.cy.tsx | 8 +- .../auto/form/AutoFormInputLabels.cy.tsx | 11 ++ .../auto/form/AutoFormUpsertAction.cy.tsx | 10 +- .../auto/form/AutoPasswordInput.cy.tsx | 5 +- packages/react/cypress/support/auto.tsx | 38 ++++- packages/react/cypress/support/commands.ts | 17 ++ .../cypress/support/cypress-commands.d.ts | 1 + packages/react/package.json | 2 + .../auto/shadcn-defaults/components/Alert.tsx | 57 ++----- .../auto/shadcn-defaults/components/Card.tsx | 79 +++++++++ .../shadcn-defaults/components/Checkbox.tsx | 26 +++ .../auto/shadcn-defaults/components/Form.tsx | 20 +-- .../auto/shadcn-defaults/components/Input.tsx | 35 ++-- .../auto/shadcn-defaults/components/Label.tsx | 31 ++-- .../shadcn-defaults/components/Select.tsx | 158 ++++++++++++++++++ .../shadcn-defaults/components/Skeleton.tsx | 5 +- .../react/spec/auto/shadcn-defaults/index.tsx | 26 ++- .../src/auto/shadcn/ShadcnAutoButton.tsx | 2 +- .../react/src/auto/shadcn/ShadcnAutoForm.tsx | 64 +++---- packages/react/src/auto/shadcn/elements.tsx | 62 ++++--- .../shadcn/inputs/ShadcnAutoBooleanInput.tsx | 59 +++++++ .../inputs/ShadcnAutoEncryptedStringInput.tsx | 32 ++++ .../shadcn/inputs/ShadcnAutoHiddenInput.tsx | 16 ++ .../auto/shadcn/inputs/ShadcnAutoIdInput.tsx | 28 ++++ .../auto/shadcn/inputs/ShadcnAutoInput.tsx | 26 ++- .../shadcn/inputs/ShadcnAutoNumberInput.tsx | 43 ++--- .../shadcn/inputs/ShadcnAutoPasswordInput.tsx | 57 +++++++ .../shadcn/inputs/ShadcnAutoTextInput.tsx | 48 +++--- .../auto/shadcn/submit/ShadcnAutoSubmit.tsx | 17 +- .../submit/ShadcnSubmitResultBanner.tsx | 21 ++- 32 files changed, 772 insertions(+), 280 deletions(-) create mode 100644 packages/react/spec/auto/shadcn-defaults/components/Card.tsx create mode 100644 packages/react/spec/auto/shadcn-defaults/components/Checkbox.tsx create mode 100644 packages/react/spec/auto/shadcn-defaults/components/Select.tsx create mode 100644 packages/react/src/auto/shadcn/inputs/ShadcnAutoBooleanInput.tsx create mode 100644 packages/react/src/auto/shadcn/inputs/ShadcnAutoEncryptedStringInput.tsx create mode 100644 packages/react/src/auto/shadcn/inputs/ShadcnAutoHiddenInput.tsx create mode 100644 packages/react/src/auto/shadcn/inputs/ShadcnAutoIdInput.tsx create mode 100644 packages/react/src/auto/shadcn/inputs/ShadcnAutoPasswordInput.tsx diff --git a/packages/react/cypress/component/auto/form/AutoForm.cy.tsx b/packages/react/cypress/component/auto/form/AutoForm.cy.tsx index 5c4fa6974..4cbcca278 100644 --- a/packages/react/cypress/component/auto/form/AutoForm.cy.tsx +++ b/packages/react/cypress/component/auto/form/AutoForm.cy.tsx @@ -36,8 +36,8 @@ describeForEachAutoAdapter("AutoForm", ({ name, adapter: { AutoForm }, wrapper } ensureFieldInputLabelsExist(); - cy.get(`input[name="widget.name"]`).type("test record"); - cy.get(`input[name="widget.inventoryCount"]`).type("999"); + cy.clickAndType(`input[name="widget.name"]`, "test record"); + cy.clickAndType(`input[name="widget.inventoryCount"]`, "999"); submit("Widget"); ensureFieldInputLabelsExist(); @@ -55,8 +55,8 @@ describeForEachAutoAdapter("AutoForm", ({ name, adapter: { AutoForm }, wrapper } const onSuccessSpy = cy.spy().as("onSuccessSpy"); cy.mountWithWrapper(, wrapper); - cy.get(`input[name="widget.name"]`).type("test record"); - cy.get(`input[name="widget.inventoryCount"]`).type("999"); + cy.clickAndType(`input[name="widget.name"]`, "test record"); + cy.clickAndType(`input[name="widget.inventoryCount"]`, "999"); cy.getSubmitButton().click(); @@ -76,8 +76,8 @@ describeForEachAutoAdapter("AutoForm", ({ name, adapter: { AutoForm }, wrapper } const onFailureSpy = cy.spy().as("onFailureSpy"); cy.mountWithWrapper(, wrapper); - cy.get(`input[name="widget.name"]`).type("test record"); - cy.get(`input[name="widget.inventoryCount"]`).type("999"); + cy.clickAndType(`input[name="widget.name"]`, "test record"); + cy.clickAndType(`input[name="widget.inventoryCount"]`, "999"); cy.getSubmitButton().click(); @@ -129,6 +129,14 @@ describeForEachAutoAdapter("AutoForm", ({ name, adapter: { AutoForm }, wrapper } }); it("can render a form to update model and submit it", () => { + + /** + * This test is disabled for Shadcn because it's not supported yet. We need to have a list component for this to work properly + */ + if (name === "Shadcn") { + return; + } + cy.intercept("POST", `${api.connection.options.endpoint}?operation=widget`, { body: { data: { @@ -172,9 +180,9 @@ describeForEachAutoAdapter("AutoForm", ({ name, adapter: { AutoForm }, wrapper } cy.contains("Anything"); // Clear the fetched value to prevent from making the value stored in the database longer as the test runs - cy.get(`input[name="widget.name"]`).clear().type("updated test record"); - cy.get(`input[name="widget.inventoryCount"]`).clear().type("1234"); - cy.get(`input[name="widget.section"]`).clear().type("Section Foo"); + cy.clickAndType(`input[name="widget.name"]`, "updated test record", true); + cy.clickAndType(`input[name="widget.inventoryCount"]`, "1234", true); + cy.clickAndType(`input[name="widget.section"]`, "Section Foo", true); cy.contains(`Section Foo`).click(); /** @@ -205,17 +213,17 @@ describeForEachAutoAdapter("AutoForm", ({ name, adapter: { AutoForm }, wrapper } ensureFieldInputLabelsExist(); // fill in name but not inventoryCount - cy.get(`input[name="widget.name"]`).type("test record"); + cy.clickAndType(`input[name="widget.name"]`, "test record"); cy.get("form [type=submit][aria-hidden!=true]").click(); cy.contains("Inventory count is required"); - cy.get(`input[name="widget.inventoryCount"]`).type("42"); + cy.clickAndType(`input[name="widget.inventoryCount"]`, "42"); - cy.get(`input[name="widget.mustBeLongString"]`).type("short"); + cy.clickAndType(`input[name="widget.mustBeLongString"]`, "short"); cy.contains("must be at least 20 characters"); - cy.get(`input[name="widget.mustBeLongString"]`).type(` l${"o".repeat(20)}ng enough`); + cy.clickAndType(`input[name="widget.mustBeLongString"]`, ` l${"o".repeat(20)}ng enough`); submit("Widget"); }); diff --git a/packages/react/cypress/component/auto/form/AutoFormDynamicFormInputChanges.cy.tsx b/packages/react/cypress/component/auto/form/AutoFormDynamicFormInputChanges.cy.tsx index 3e3e64ce0..3d5a59522 100644 --- a/packages/react/cypress/component/auto/form/AutoFormDynamicFormInputChanges.cy.tsx +++ b/packages/react/cypress/component/auto/form/AutoFormDynamicFormInputChanges.cy.tsx @@ -10,10 +10,10 @@ describeForEachAutoAdapter("AutoForm", ({ name, adapter: { AutoForm }, wrapper } return ( <> - +
-
); @@ -57,8 +57,12 @@ describeForEachAutoAdapter("AutoForm", ({ name, adapter: { AutoForm }, wrapper } cy.get(`input[name="widget.name"]`).should("have.value", "test record 1"); cy.get(`input[name="widget.inventoryCount"]`).should("have.value", "1"); - cy.get(`input[name="widget.name"]`).type("Dirty the value"); - cy.get(`input[name="widget.inventoryCount"]`).type("123546"); + + cy.clickAndType(`input[name="widget.name"]`, "Dirty the value"); + + + + cy.clickAndType(`input[name="widget.inventoryCount"]`, "123546"); cy.get("#setFindById2").click(); diff --git a/packages/react/cypress/component/auto/form/AutoFormGlobalActions.cy.tsx b/packages/react/cypress/component/auto/form/AutoFormGlobalActions.cy.tsx index 2ad7de1f5..bb6ad8cc6 100644 --- a/packages/react/cypress/component/auto/form/AutoFormGlobalActions.cy.tsx +++ b/packages/react/cypress/component/auto/form/AutoFormGlobalActions.cy.tsx @@ -13,8 +13,8 @@ describeForEachAutoAdapter("AutoForm - Global actions", ({ name, adapter: { Auto cy.get(`input[name="title"]`).should("have.value", ""); cy.get(`input[name="inventoryCount"]`).should("have.value", ""); - cy.get(`input[name="title"]`).type("foo"); - cy.get(`input[name="inventoryCount"]`).type("42"); + cy.clickAndType(`input[name="title"]`, "foo"); + cy.clickAndType(`input[name="inventoryCount"]`, "42"); cy.intercept("POST", `${api.connection.options.endpoint}?operation=flipAll`, { body: { @@ -45,8 +45,8 @@ describeForEachAutoAdapter("AutoForm - Global actions", ({ name, adapter: { Auto cy.get(`input[name="title"]`).should("have.value", ""); cy.get(`input[name="inventoryCount"]`).should("have.value", ""); - cy.get(`input[name="title"]`).type("foo"); - cy.get(`input[name="inventoryCount"]`).type("42"); + cy.clickAndType(`input[name="title"]`, "foo"); + cy.clickAndType(`input[name="inventoryCount"]`, "42"); cy.intercept("POST", `${api.connection.options.endpoint}?operation=flipAll`, { times: 1, diff --git a/packages/react/cypress/component/auto/form/AutoFormInputLabels.cy.tsx b/packages/react/cypress/component/auto/form/AutoFormInputLabels.cy.tsx index dc118ac8e..28e9c4943 100644 --- a/packages/react/cypress/component/auto/form/AutoFormInputLabels.cy.tsx +++ b/packages/react/cypress/component/auto/form/AutoFormInputLabels.cy.tsx @@ -1,15 +1,26 @@ /* eslint-disable jest/valid-expect */ import React from "react"; import { PolarisAutoInput } from "../../../../src/auto/polaris/inputs/PolarisAutoInput.js"; +import { makeShadcnAutoInput } from "../../../../src/auto/shadcn/inputs/ShadcnAutoInput.js"; import { humanizeCamelCase } from "../../../../src/utils.js"; import { api } from "../../../support/api.js"; import { describeForEachAutoAdapter } from "../../../support/auto.js"; +import { Button } from "../../../../spec/auto/shadcn-defaults/components/Button.js"; +import { Checkbox } from "../../../../spec/auto/shadcn-defaults/components/Checkbox.js"; +import { Input } from "../../../../spec/auto/shadcn-defaults/components/Input.js"; +import { Label } from "../../../../spec/auto/shadcn-defaults/components/Label.js"; + const AutoInput = (props: { suiteName: string; field: string; label?: string }) => { if (props.suiteName === "Polaris") { return ; } + if (props.suiteName === "Shadcn") { + const ShadcnAutoInput = makeShadcnAutoInput({ Input: Input, Label: Label, Checkbox: Checkbox, Button: Button }); + return ; + } + throw new Error("Invalid suite name"); }; diff --git a/packages/react/cypress/component/auto/form/AutoFormUpsertAction.cy.tsx b/packages/react/cypress/component/auto/form/AutoFormUpsertAction.cy.tsx index 24eed6f4a..4107cd4e6 100644 --- a/packages/react/cypress/component/auto/form/AutoFormUpsertAction.cy.tsx +++ b/packages/react/cypress/component/auto/form/AutoFormUpsertAction.cy.tsx @@ -73,8 +73,8 @@ describeForEachAutoAdapter("AutoForm - Upsert Action", ({ name, adapter: { AutoF }; const populateRequiredFields = () => { - cy.get(`input[name="widget.name"]`).clear().type("name"); - cy.get(`input[name="widget.inventoryCount"]`).clear().type("123"); + cy.clickAndType(`input[name="widget.name"]`, "name", true); + cy.clickAndType(`input[name="widget.inventoryCount"]`, "123", true); }; beforeEach(() => { @@ -93,10 +93,12 @@ describeForEachAutoAdapter("AutoForm - Upsert Action", ({ name, adapter: { AutoF populateRequiredFields(); // Does not allow submission when the ID input does not have a positive integer value - cy.get(`input[name="widget.id"]`).clear().type("-1{enter}"); + cy.clickAndType(`input[name="widget.id"]`, "-1", true); + if (upsertHasBeenCalled) throw new Error("Upsert was called when it shouldn't have been"); - cy.get(`input[name="widget.id"]`).clear().type("1.1{enter}"); + cy.clickAndType(`input[name="widget.id"]`, "1.1", true); + if (upsertHasBeenCalled) throw new Error("Upsert was called when it shouldn't have been"); cy.get(`input[name="widget.id"]`).clear().type("1{enter}"); diff --git a/packages/react/cypress/component/auto/form/AutoPasswordInput.cy.tsx b/packages/react/cypress/component/auto/form/AutoPasswordInput.cy.tsx index 0c82b0555..20bb07b05 100644 --- a/packages/react/cypress/component/auto/form/AutoPasswordInput.cy.tsx +++ b/packages/react/cypress/component/auto/form/AutoPasswordInput.cy.tsx @@ -54,8 +54,11 @@ describeForEachAutoAdapter("AutoPasswordInput", ({ name, adapter: { AutoForm }, cy.get(`button[role="passwordEditPasswordButton"]`).first().click(); // Enabled after clicking the edit button + cy.get(`input[name="user.password"]`).should("be.enabled"); - cy.get(`input[name="user.password"]`).type(updatedPassword); + + cy.clickAndType(`input[name="user.password"]`, updatedPassword); + expectUpdateActionSubmissionVariables(expectedVariables); // Password field is changed and included submit("User", expectedVariables); diff --git a/packages/react/cypress/support/auto.tsx b/packages/react/cypress/support/auto.tsx index 2b374b33f..c3199f7e2 100644 --- a/packages/react/cypress/support/auto.tsx +++ b/packages/react/cypress/support/auto.tsx @@ -15,6 +15,25 @@ interface AutoSuiteConfig { wrapper: ComponentType<{ children: ReactNode }>; } +const ONLY_RUN_SUITES = { + "Shadcn": ["AutoForm", + "AutoButton", + "AutoForm - input labels", + "AutoForm - Default model field values", + "AutoForm - FindBy object parameters", + "AutoForm - Global actions", + "AutoForm - HasManyThrough fields", + "AutoForm - Dynamic form input changes", + "AutoForm - Dynamic form input changes - FindBy object parameters", + "AutoForm - Dynamic form input changes - Global actions", + "AutoForm - Dynamic form input changes - HasManyThrough fields", + "AutoForm titles", + "AutoForm - ID field", + "AutoForm - Upsert Action", + "AutoPasswordInput" + ] +}; + export const PolarisWrapper = ({ children }: { children: ReactNode }) => ( @@ -25,25 +44,32 @@ export const PolarisWrapper = ({ children }: { children: ReactNode }) => ( ); - const ShadCNAdapter = makeAutocomponents({ ...elements }); export const ShadcnWrapper = ({ children }: { children: ReactNode }) => ( <> - - {children} + + + + {children} + + ); const suites: AutoSuiteConfig[] = [ -// { name: "Polaris", adapter: PolarisAdapter as any, wrapper: PolarisWrapper }, + { name: "Polaris", adapter: PolarisAdapter as any, wrapper: PolarisWrapper }, { name: "Shadcn", adapter: ShadCNAdapter as any, wrapper: ShadcnWrapper }, ]; export const adapters = [PolarisAdapter]; + export const describeForEachAutoAdapter = (suiteName: string, suite: (config: AutoSuiteConfig) => void) => { - // eslint-disable-next-line jest/valid-describe-callback, jest/valid-title - describe.each(suites)((({ name }: { name: string }) => `${suiteName} - ${name}`) as any, suite); + const filteredSuites = suites.filter(config => + config.name !== "Shadcn" || ONLY_RUN_SUITES["Shadcn"].includes(suiteName) + ); + + describe.each(filteredSuites)((({ name }: { name: string }) => `${suiteName} - ${name}`) as any, suite); }; diff --git a/packages/react/cypress/support/commands.ts b/packages/react/cypress/support/commands.ts index c2a53d6aa..ed633ae71 100644 --- a/packages/react/cypress/support/commands.ts +++ b/packages/react/cypress/support/commands.ts @@ -93,3 +93,20 @@ Cypress.Commands.add("mockUploadFile", (customToken) => { Cypress.Commands.add("getSubmitButton", () => { return cy.get("form [type=submit][aria-hidden!=true]"); }); + +/** + * Click on an element and type text into it + * This command is used to simulate user interaction with the UI due to the fact that Cypress kept seeing some input elements as disabled + * more on the issue here: https://github.com/cypress-io/cypress/issues/5830 and https://github.com/cypress-io/cypress/issues/5827 + * + * @param selector - The selector of the element to click and type into + * @param text - The text to type into the element + * @param clear - Whether to clear the element before typing + */ +Cypress.Commands.add("clickAndType", { prevSubject: false }, (selector: string, text: string, clear = false) => { + cy.get(selector).click(); + if (clear) { + cy.get(selector).clear(); + } + cy.get(selector).type(text); +}); diff --git a/packages/react/cypress/support/cypress-commands.d.ts b/packages/react/cypress/support/cypress-commands.d.ts index e214d4dfa..0ce2a5e19 100644 --- a/packages/react/cypress/support/cypress-commands.d.ts +++ b/packages/react/cypress/support/cypress-commands.d.ts @@ -20,5 +20,6 @@ declare namespace Cypress { mockGetDirectUploadToken(api: any, customToken?: string): Chainable; mockUploadFile: (customToken?: string) => Chainable; getSubmitButton: () => Chainable>; + clickAndType: (selector: string, text: string, clear?: boolean) => Chainable; } } diff --git a/packages/react/package.json b/packages/react/package.json index 88482a263..9f5db0c87 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -74,8 +74,10 @@ "@pollyjs/adapter-xhr": "^6.0.6", "@pollyjs/core": "^6.0.6", "@pollyjs/persister-fs": "^6.0.6", + "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-toast": "^1.2.4", + "@radix-ui/react-select": "^2.1.4", "@shopify/polaris": "^12.0.0", "@shopify/polaris-icons": "^8.1.0", "@storybook/addon-essentials": "^8.1.6", diff --git a/packages/react/spec/auto/shadcn-defaults/components/Alert.tsx b/packages/react/spec/auto/shadcn-defaults/components/Alert.tsx index ee07bf1d0..e7c8fe595 100644 --- a/packages/react/spec/auto/shadcn-defaults/components/Alert.tsx +++ b/packages/react/spec/auto/shadcn-defaults/components/Alert.tsx @@ -1,5 +1,5 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; import { cn } from "../utils.js"; @@ -9,51 +9,28 @@ const alertVariants = cva( variants: { variant: { default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", }, }, defaultVariants: { variant: "default", }, } -) +); -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({ className, variant, ...props }, ref) => ( -
-)) -Alert.displayName = "Alert" +const Alert = React.forwardRef & VariantProps>( + ({ className, variant, ...props }, ref) =>
+); +Alert.displayName = "Alert"; -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertTitle.displayName = "AlertTitle" +const AlertTitle = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertDescription.displayName = "AlertDescription" +const AlertDescription = React.forwardRef>( + ({ className, ...props }, ref) =>
+); +AlertDescription.displayName = "AlertDescription"; -export { Alert, AlertTitle, AlertDescription } +export { Alert, AlertDescription, AlertTitle }; diff --git a/packages/react/spec/auto/shadcn-defaults/components/Card.tsx b/packages/react/spec/auto/shadcn-defaults/components/Card.tsx new file mode 100644 index 000000000..a2caefc28 --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/components/Card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "../utils.js" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } \ No newline at end of file diff --git a/packages/react/spec/auto/shadcn-defaults/components/Checkbox.tsx b/packages/react/spec/auto/shadcn-defaults/components/Checkbox.tsx new file mode 100644 index 000000000..092fd271f --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/components/Checkbox.tsx @@ -0,0 +1,26 @@ +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; +import * as React from "react"; + +import { cn } from "../utils.js"; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/packages/react/spec/auto/shadcn-defaults/components/Form.tsx b/packages/react/spec/auto/shadcn-defaults/components/Form.tsx index 410d27fda..243c04da4 100644 --- a/packages/react/spec/auto/shadcn-defaults/components/Form.tsx +++ b/packages/react/spec/auto/shadcn-defaults/components/Form.tsx @@ -1,21 +1,9 @@ -import { cn } from "../utils.js"; import * as React from "react"; +import { cn } from "../utils.js"; -const Form = React.forwardRef< - HTMLFormElement, - React.FormHTMLAttributes ->(({ className, ...props }, ref) => { - return ( -
- ); +const Form = React.forwardRef>(({ className, ...props }, ref) => { + return ; }); Form.displayName = "Form"; -export { Form }; \ No newline at end of file +export { Form }; diff --git a/packages/react/spec/auto/shadcn-defaults/components/Input.tsx b/packages/react/spec/auto/shadcn-defaults/components/Input.tsx index 1e1c2a1a6..83c4234d9 100644 --- a/packages/react/spec/auto/shadcn-defaults/components/Input.tsx +++ b/packages/react/spec/auto/shadcn-defaults/components/Input.tsx @@ -1,23 +1,20 @@ -import * as React from "react" +import * as React from "react"; import { cn } from "../utils.js"; -const Input = React.forwardRef>( - ({ className, type, ...props }, ref) => { +const Input = React.forwardRef>(({ className, type, ...props }, ref) => { + return ( + + ); +}); +Input.displayName = "Input"; - return ( - - ) - } -) -Input.displayName = "Input" - -export { Input } +export { Input }; diff --git a/packages/react/spec/auto/shadcn-defaults/components/Label.tsx b/packages/react/spec/auto/shadcn-defaults/components/Label.tsx index 9b456c795..5c7881614 100644 --- a/packages/react/spec/auto/shadcn-defaults/components/Label.tsx +++ b/packages/react/spec/auto/shadcn-defaults/components/Label.tsx @@ -1,25 +1,14 @@ -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { cva, type VariantProps } from "class-variance-authority" +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; import { cn } from "../utils.js"; - - -const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" -) - +const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"); + const Label = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, ...props }, ref) => ( - -)) -Label.displayName = LabelPrimitive.Root.displayName - -export { Label } \ No newline at end of file + React.ComponentPropsWithoutRef & VariantProps +>(({ className, ...props }, ref) => ); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/packages/react/spec/auto/shadcn-defaults/components/Select.tsx b/packages/react/spec/auto/shadcn-defaults/components/Select.tsx new file mode 100644 index 000000000..c119643dd --- /dev/null +++ b/packages/react/spec/auto/shadcn-defaults/components/Select.tsx @@ -0,0 +1,158 @@ +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "../utils.js" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} \ No newline at end of file diff --git a/packages/react/spec/auto/shadcn-defaults/components/Skeleton.tsx b/packages/react/spec/auto/shadcn-defaults/components/Skeleton.tsx index f5b7ed91c..a9ee07384 100644 --- a/packages/react/spec/auto/shadcn-defaults/components/Skeleton.tsx +++ b/packages/react/spec/auto/shadcn-defaults/components/Skeleton.tsx @@ -1,8 +1,7 @@ -import React from "react"; import { cva, type VariantProps } from "class-variance-authority"; +import React from "react"; import { cn } from "../utils.js"; - const skeletonVariants = cva("animate-pulse rounded-md bg-primary/10", { variants: { size: { @@ -29,4 +28,4 @@ const Skeleton = React.forwardRef(({ className, s Skeleton.displayName = "Skeleton"; -export { Skeleton, skeletonVariants }; \ No newline at end of file +export { Skeleton, skeletonVariants }; diff --git a/packages/react/spec/auto/shadcn-defaults/index.tsx b/packages/react/spec/auto/shadcn-defaults/index.tsx index c6184fd85..98ee4dbf6 100644 --- a/packages/react/spec/auto/shadcn-defaults/index.tsx +++ b/packages/react/spec/auto/shadcn-defaults/index.tsx @@ -1,23 +1,33 @@ import type { ShadcnElements } from "../../../src/auto/shadcn/elements.js"; -import { Alert, AlertTitle, AlertDescription } from "./components/Alert.js"; +import { Alert, AlertDescription, AlertTitle } from "./components/Alert.js"; import { Button } from "./components/Button.js"; -import { Label } from "./components/Label.js"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./components/Card.js"; +import { Checkbox } from "./components/Checkbox.js"; import { Form } from "./components/Form.js"; +import { Select } from "./components/Select.js"; import { Input } from "./components/Input.js"; +import { Label } from "./components/Label.js"; import { Skeleton } from "./components/Skeleton.js"; import { toast } from "./hooks/useToast.js"; - export const elements: ShadcnElements = { + Alert, + AlertDescription, + AlertTitle, Button, - Skeleton, - toast, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + Checkbox, Form, Input, - Alert, - AlertTitle, - AlertDescription, Label, + Select, + Skeleton, + toast, }; export { Toaster } from "./components/Toaster.js"; diff --git a/packages/react/src/auto/shadcn/ShadcnAutoButton.tsx b/packages/react/src/auto/shadcn/ShadcnAutoButton.tsx index e19584e1d..230162087 100644 --- a/packages/react/src/auto/shadcn/ShadcnAutoButton.tsx +++ b/packages/react/src/auto/shadcn/ShadcnAutoButton.tsx @@ -34,7 +34,7 @@ export const makeAutoButton = }); return ( - ); diff --git a/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx b/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx index 83db38d38..189392f9e 100644 --- a/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx +++ b/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx @@ -7,41 +7,36 @@ import { useAutoForm } from "../AutoForm.js"; import { validateAutoFormProps } from "../AutoFormActionValidators.js"; import { AutoFormMetadataContext } from "../AutoFormContext.js"; import type { FormProps, ShadcnElements } from "./elements.js"; -import { makeSubmitResultBanner } from "./submit/ShadcnSubmitResultBanner.js"; import { makeShadcnAutoInput } from "./inputs/ShadcnAutoInput.js"; import { makeShadcnAutoSubmit } from "./submit/ShadcnAutoSubmit.js"; +import { makeSubmitResultBanner } from "./submit/ShadcnSubmitResultBanner.js"; /** * Renders a form for an action on a model automatically using Shadcn */ export const makeAutoForm = - ({ Form, Input, Button, Alert, Skeleton, AlertTitle, AlertDescription, Label }: Elements) => - < - GivenOptions extends OptionsType, - SchemaT, - ActionFunc extends ActionFunction - >( - props: AutoFormProps & ComponentProps - ) => { - const { action, findBy } = props as AutoFormProps & - Omit, "action"> & { findBy: any }; - - validateAutoFormProps(props); + ({ Form, Input, Button, Alert, Skeleton, AlertTitle, AlertDescription, Label, Checkbox }: Elements) => + >( + props: AutoFormProps & ComponentProps + ) => { + const { action, findBy } = props as AutoFormProps & + Omit, "action"> & { findBy: any }; - // Component key to force re-render when the action or findBy changes - const componentKey = `${action.modelApiIdentifier ?? ""}.${action.operationName}.${JSON.stringify(findBy)}`; + validateAutoFormProps(props); - const ShadcnAutoInput = makeShadcnAutoInput({ Input, Label }); + // Component key to force re-render when the action or findBy changes + const componentKey = `${action.modelApiIdentifier ?? ""}.${action.operationName}.${JSON.stringify(findBy)}`; - return ( - & Omit, "action"> & { findBy: any })} - elements={{ Form, Input, Button, Alert, Skeleton, AlertTitle, AlertDescription, ShadcnAutoInput }} - /> - ); - } + const ShadcnAutoInput = makeShadcnAutoInput({ Input, Label, Button, Checkbox }); + return ( + & Omit, "action"> & { findBy: any })} + elements={{ Form, Input, Button, Alert, Skeleton, AlertTitle, AlertDescription, ShadcnAutoInput }} + /> + ); + }; const ShadcnAutoFormComponent = < GivenOptions extends OptionsType, @@ -58,7 +53,7 @@ const ShadcnAutoFormComponent = < findBy, ...rest } = props as AutoFormProps & Omit, "action"> & { findBy: any }; - const { Form, Button, Alert, Skeleton, AlertTitle, AlertDescription, ShadcnAutoInput } = props.elements; + const { Form, Button, Alert, Skeleton, AlertTitle, AlertDescription, ShadcnAutoInput } = props.elements; const { metadata, fetchingMetadata, metadataError, fields, submit, formError, isSubmitting, isSubmitSuccessful, originalFormMethods } = useAutoForm(props); @@ -66,7 +61,6 @@ const ShadcnAutoFormComponent = < const { ShadcnSubmitSuccessfulBanner, ShadcnSubmitErrorBanner } = makeSubmitResultBanner({ Alert, AlertTitle, AlertDescription }); const ShadcnAutoSubmit = makeShadcnAutoSubmit({ Button }); - const autoFormMetadataContext: AutoFormMetadataContext = { findBy, submit, @@ -88,7 +82,6 @@ const ShadcnAutoFormComponent = < const formTitle = props.title === undefined ? humanizeCamelCase(action.operationName) : props.title; - if (props.successContent && isSubmitSuccessful) { return props.successContent; } @@ -100,25 +93,18 @@ const ShadcnAutoFormComponent = < ); } - + const formContent = props.children ?? ( <> - {formTitle && ( -

{formTitle}

- )} + {formTitle &&

{formTitle}

} {!props.onSuccess && } {!props.onFailure && } {!metadataError && ( <> {fields.map(({ metadata }) => ( - + ))} - - {props.submitLabel ?? "Submit"} - + {props.submitLabel ?? "Submit"} )} @@ -133,4 +119,4 @@ const ShadcnAutoFormComponent = < ); -} \ No newline at end of file +}; diff --git a/packages/react/src/auto/shadcn/elements.tsx b/packages/react/src/auto/shadcn/elements.tsx index 5b7bb0156..c943e98f5 100644 --- a/packages/react/src/auto/shadcn/elements.tsx +++ b/packages/react/src/auto/shadcn/elements.tsx @@ -1,50 +1,46 @@ +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; import type React from "react"; import type { PropsWithChildren } from "react"; - /** The props that a component injected into autocomponent's shadcn must support */ export interface BaseProps extends PropsWithChildren { - variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"; + variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null; className?: string; } /** The props that a button component injected into autocomponent's shadcn must support */ -export interface ButtonProps - extends BaseProps, - React.ButtonHTMLAttributes { +export interface ButtonProps extends BaseProps, React.ButtonHTMLAttributes { asChild?: boolean; + size?: "default" | "sm" | "lg" | "icon" | null; } /** The props that a skeleton component injected into autocomponent's shadcn must support */ -export interface SkeletonProps extends Pick { -} +export interface SkeletonProps extends Pick {} /** The props that an alert component injected into autocomponent's shadcn must support */ export interface AlertProps extends Omit { - variant?: "default" | "destructive"; + variant?: "default" | "destructive" | null; } /** The props that an alert title component injected into autocomponent's shadcn must support */ -export interface AlertTitleProps extends Pick { -} +export interface AlertTitleProps extends Pick {} /** The props that an alert description component injected into autocomponent's shadcn must support */ -export interface AlertDescriptionProps extends Pick { -} +export interface AlertDescriptionProps extends Pick {} /** The props that a form component injected into autocomponent's shadcn must support */ -export interface FormProps extends Pick { -} +export interface FormProps extends Pick {} /** The props that an input component injected into autocomponent's shadcn must support */ -export interface InputProps extends Pick, React.InputHTMLAttributes { -} +export interface InputProps extends Pick, React.InputHTMLAttributes {} /** The props that a label component injected into autocomponent's shadcn must support */ -export interface LabelProps extends React.LabelHTMLAttributes { -} - +export interface LabelProps extends React.LabelHTMLAttributes {} +/** The props that a checkbox component injected into autocomponent's shadcn must support */ +export interface CheckboxProps extends Omit, "type"> { + checked?: boolean | "indeterminate"; +} /** One toast for showing via the toasting system */ export interface ToasterToast extends BaseProps { @@ -53,18 +49,19 @@ export interface ToasterToast extends BaseProps { action?: React.ReactNode; } - - export interface ShadcnElements { /** The Label component from shadcn */ Label: React.ComponentType; /** The Button component from shadcn */ - Button: React.ComponentType; + Button: React.ForwardRefExoticComponent>; /** The Skeleton component from shadcn */ Skeleton: React.ComponentType; /** The Alert component from shadcn */ Alert: React.ComponentType; + /** The Checkbox component from shadcn */ + Checkbox: React.ForwardRefExoticComponent>; + /** The Form component from shadcn */ Form: React.ComponentType; @@ -75,6 +72,25 @@ export interface ShadcnElements { AlertTitle: React.ComponentType; /** The AlertDescription component from shadcn */ AlertDescription: React.ComponentType; + /** The toast imperative function from shadcn */ toast: (props: ToasterToast) => void; - } + + /** The Card component from shadcn */ + Card: React.ComponentType>; + + /** The CardHeader component from shadcn */ + CardHeader: React.ComponentType>; + + /** The CardFooter component from shadcn */ + CardFooter: React.ComponentType>; + + /** The CardTitle component from shadcn */ + CardTitle: React.ComponentType>; + + /** The CardDescription component from shadcn */ + CardDescription: React.ComponentType>; + + /** The CardContent component from shadcn */ + CardContent: React.ComponentType>; +} diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoBooleanInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoBooleanInput.tsx new file mode 100644 index 000000000..434c28216 --- /dev/null +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoBooleanInput.tsx @@ -0,0 +1,59 @@ +import React, { useEffect } from "react"; +import type { Control } from "../../../useActionForm.js"; +import { useController, useFormContext } from "../../../useActionForm.js"; +import { get } from "../../../utils.js"; +import { autoInput } from "../../AutoInput.js"; +import { useFieldMetadata } from "../../hooks/useFieldMetadata.js"; +// should we move this to a shared location? +import { cn } from "../../../../spec/auto/shadcn-defaults/utils.js"; +import type { CheckboxProps, ShadcnElements } from "../elements.js"; + +export const makeShadcnAutoBooleanInput = ({ Checkbox, Label }: Pick) => { + return autoInput( + ( + props: { + field: string; + control?: Control; + className?: string; + label?: string; + } & Partial + ) => { + const { field: fieldApiIdentifier, control, ...rest } = props; + const { path, metadata } = useFieldMetadata(fieldApiIdentifier); + + const { + field: fieldProps, + fieldState: { error }, + } = useController({ + control, + name: path, + }); + + const { + formState: { defaultValues }, + } = useFormContext(); + + useEffect(() => { + if (metadata.requiredArgumentForInput) { + // when the field is required, this defaults to false to match the UI + // When not required, the field will have a null value unless it is touched by the user + const defaultValue = get(defaultValues ?? {}, path) ?? false; + fieldProps.onChange(defaultValue); + } + }, [metadata.requiredArgumentForInput, defaultValues]); + + const label = props.label ?? metadata.name; + const { value: _value, ...restFieldProps } = fieldProps; + + return ( +
+ + + {error && {error.message}} +
+ ); + } + ); +}; diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoEncryptedStringInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoEncryptedStringInput.tsx new file mode 100644 index 000000000..503d1a97a --- /dev/null +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoEncryptedStringInput.tsx @@ -0,0 +1,32 @@ +import { EyeIcon, EyeOffIcon } from "lucide-react"; +import React, { useState } from "react"; +import type { Control } from "../../../useActionForm.js"; +import { autoInput } from "../../AutoInput.js"; +import type { ShadcnElements } from "../elements.js"; +import { makeShadcnAutoTextInput } from "./ShadcnAutoTextInput.js"; + +export const makeShadcnAutoEncryptedStringInput = ({ Input, Label, Button }: Pick) => { + const TextInput = makeShadcnAutoTextInput({ Input, Label }); + + return autoInput((props: { field: string; control?: Control; className?: string; suffix?: React.ReactNode }) => { + const [isShown, setIsShown] = useState(false); + const { suffix, ...restProps } = props; + + const showHideToggleButton = ( + + ); + + return ( + + ); + }); +}; diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoHiddenInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoHiddenInput.tsx new file mode 100644 index 000000000..bc89aaf59 --- /dev/null +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoHiddenInput.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { autoInput } from "../../AutoInput.js"; +import { useHiddenInput } from "../../hooks/useHiddenInput.js"; +import type { ShadcnElements } from "../elements.js"; + +export const makeShadcnAutoHiddenInput = ({ Input }: Pick) => + autoInput( + (props: { + field: string; // The field API identifier + value: any; + }) => { + const fieldProps = useHiddenInput(props); + + return ; + } + ); diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoIdInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoIdInput.tsx new file mode 100644 index 000000000..9f2d26361 --- /dev/null +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoIdInput.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { FieldType } from "../../../metadata.js"; +import { autoInput } from "../../AutoInput.js"; +import { useStringInputController } from "../../hooks/useStringInputController.js"; +import type { ShadcnElements } from "../elements.js"; +import { makeShadcnAutoTextInput } from "./ShadcnAutoTextInput.js"; + +export const makeShadcnAutoIdInput = ({ Input, Label }: Pick) => { + const ShadcnAutoTextInput = makeShadcnAutoTextInput({ Input, Label }); + + return autoInput( + (props: { + field: string; // The field API identifier + label?: string; + }) => { + const { field, label } = props; + const { name, metadata } = useStringInputController({ field }); + + if (metadata.fieldType !== FieldType.Id || field !== "id") { + throw new Error(`ShadcnAutoIdInput: field ${field} is not of type Id`); + } + + console.log("name", name, label, "Rendering ShadcnAutoIdInput", props); + + return ; + } + ); +}; diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoInput.tsx index aca0b37d0..786c82832 100644 --- a/packages/react/src/auto/shadcn/inputs/ShadcnAutoInput.tsx +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoInput.tsx @@ -1,17 +1,23 @@ -import React from "react"; import { FieldType } from "../../../metadata.js"; import { autoInput } from "../../AutoInput.js"; import { useFieldMetadata } from "../../hooks/useFieldMetadata.js"; import type { ShadcnElements } from "../elements.js"; -import { makeShadcnAutoTextInput } from "./ShadcnAutoTextInput.js"; +import { makeShadcnAutoBooleanInput } from "./ShadcnAutoBooleanInput.js"; +import { makeShadcnAutoEncryptedStringInput } from "./ShadcnAutoEncryptedStringInput.js"; +import { makeShadcnAutoIdInput } from "./ShadcnAutoIdInput.js"; import { makeShadcnAutoNumberInput } from "./ShadcnAutoNumberInput.js"; +import { makeShadcnAutoPasswordInput } from "./ShadcnAutoPasswordInput.js"; +import { makeShadcnAutoTextInput } from "./ShadcnAutoTextInput.js"; -export const makeShadcnAutoInput = ({ Input, Label }: Pick) => +export const makeShadcnAutoInput = ({ Input, Label, Button, Checkbox }: Pick) => autoInput((props: { field: string; label?: string }) => { const { metadata } = useFieldMetadata(props.field); const config = metadata.configuration; - + switch (config.fieldType) { + case FieldType.Id: { + return makeShadcnAutoIdInput({ Input, Label })(props); + } case FieldType.String: case FieldType.Email: case FieldType.Color: @@ -21,8 +27,18 @@ export const makeShadcnAutoInput = ({ Input, Label }: Pick) => { - const TextInput = makeShadcnAutoTextInput({ Input, Label }); + const ShadcnAutoTextInput = makeShadcnAutoTextInput({ Input, Label }); - return autoInput((props: { - field: string; - control?: Control; - className?: string; - }) => { + return autoInput((props: { field: string; control?: Control; className?: string }) => { const { field, control } = props; const { metadata, value } = useStringInputController({ field, control }); const step = - metadata.configuration.__typename === "GadgetNumberConfig" && - metadata.configuration.decimals && - metadata.configuration.decimals > 0 + metadata.configuration.__typename === "GadgetNumberConfig" && metadata.configuration.decimals && metadata.configuration.decimals > 0 ? getStepFromNumberOfDecimals(metadata.configuration.decimals) : value ? getStepFromNumberOfDecimals(countNumberOfDecimals(`${value}`)) : 1; - return ( - - ); + return ; }); }; // TODO: move to a shared location - const getStepFromNumberOfDecimals = (numberOfDecimals: number) => - numberOfDecimals === 0 ? 1 : Number(`0.${"0".repeat(numberOfDecimals - 1)}1`); - - const countNumberOfDecimals = (value: string) => { - if (value.includes("e")) { - // +e scientific notation for large numbers does not get decimal steps - return 0; - } - const [, decimals] = value.split("."); - return decimals?.length ?? 0; - }; \ No newline at end of file +const getStepFromNumberOfDecimals = (numberOfDecimals: number) => + numberOfDecimals === 0 ? 1 : Number(`0.${"0".repeat(numberOfDecimals - 1)}1`); + +const countNumberOfDecimals = (value: string) => { + if (value.includes("e")) { + // +e scientific notation for large numbers does not get decimal steps + return 0; + } + const [, decimals] = value.split("."); + return decimals?.length ?? 0; +}; diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoPasswordInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoPasswordInput.tsx new file mode 100644 index 000000000..d807da8d2 --- /dev/null +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoPasswordInput.tsx @@ -0,0 +1,57 @@ +import { PencilIcon } from "lucide-react"; +import React, { useState } from "react"; +import type { Control } from "../../../useActionForm.js"; +import { useController } from "../../../useActionForm.js"; +import { useAutoFormMetadata } from "../../AutoFormContext.js"; +import { autoInput } from "../../AutoInput.js"; +import { useFieldMetadata } from "../../hooks/useFieldMetadata.js"; +import type { ShadcnElements } from "../elements.js"; +import { makeShadcnAutoEncryptedStringInput } from "./ShadcnAutoEncryptedStringInput.js"; + +/** + * The salted password hash is not retrieved from the DB + * Regardless of the password is defined or not, this placeholder is shown as exposing an unset password is a security risk + */ +const existingPasswordPlaceholder = "********"; + +export const makeShadcnAutoPasswordInput = ({ Input, Label, Button }: Pick) => { + const EncryptedInput = makeShadcnAutoEncryptedStringInput({ Input, Label, Button }); + + return autoInput((props: { field: string; control?: Control; className?: string }) => { + const { findBy } = useAutoFormMetadata(); + const { path } = useFieldMetadata(props.field); + const { field: fieldProps } = useController({ name: path }); + + const [isEditing, setIsEditing] = useState(!findBy); + + const startEditing = () => { + fieldProps.onChange(""); // Touch the field to mark it as dirty + setIsEditing(true); + }; + + return ( + + + + ), + })} + /> + ); + }); +}; diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx index 6f8c4c50e..5fd1a5fee 100644 --- a/packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx @@ -1,5 +1,5 @@ -import React from "react"; import type { InputHTMLAttributes } from "react"; +import React from "react"; import type { Control } from "../../../useActionForm.js"; import { getPropsWithoutRef } from "../../../utils.js"; import { autoInput } from "../../AutoInput.js"; @@ -11,33 +11,41 @@ import { cn } from "../../../../spec/auto/shadcn-defaults/utils.js"; export const makeShadcnAutoTextInput = ({ Input, Label }: Pick) => autoInput( - (props: { - field: string; // The field API identifier - control?: Control; - } & Partial>) => { - const { field, control } = props; + ( + props: { + field: string; // The field API identifier + control?: Control; + label?: string; + suffix?: React.ReactNode; + } & Partial> + ) => { + const { field, control, label: customLabel, suffix, ...restProps } = props; const stringInputController = useStringInputController({ field, control }); const id = `${field}-input`; - const label = stringInputController.label || stringInputController.metadata.name; - + const label = customLabel || stringInputController.label || stringInputController.metadata.name; return (
- + - {stringInputController.errorMessage && ( -

{stringInputController.errorMessage}

- )} + {suffix && ( +
+ {suffix} +
+ )} +
+ {stringInputController.errorMessage &&

{stringInputController.errorMessage}

}
); } - ); \ No newline at end of file + ); diff --git a/packages/react/src/auto/shadcn/submit/ShadcnAutoSubmit.tsx b/packages/react/src/auto/shadcn/submit/ShadcnAutoSubmit.tsx index 5031d5801..f7b8395a6 100644 --- a/packages/react/src/auto/shadcn/submit/ShadcnAutoSubmit.tsx +++ b/packages/react/src/auto/shadcn/submit/ShadcnAutoSubmit.tsx @@ -1,26 +1,17 @@ -import React from "react"; import type { ReactNode } from "react"; +import React from "react"; import { useAutoFormMetadata } from "../../AutoFormContext.js"; import type { ShadcnElements } from "../elements.js"; export const makeShadcnAutoSubmit = ({ Button }: Pick) => { - - return function ShadcnAutoSubmit(props: { - children?: ReactNode; - isSubmitting?: boolean; - className?: string; - }) { + return function ShadcnAutoSubmit(props: { children?: ReactNode; isSubmitting?: boolean; className?: string }) { const { submitResult } = useAutoFormMetadata(); const isSubmitting = props.isSubmitting ?? submitResult.isSubmitting; return ( - ); }; -}; \ No newline at end of file +}; diff --git a/packages/react/src/auto/shadcn/submit/ShadcnSubmitResultBanner.tsx b/packages/react/src/auto/shadcn/submit/ShadcnSubmitResultBanner.tsx index 70e66b835..643c4d9e6 100644 --- a/packages/react/src/auto/shadcn/submit/ShadcnSubmitResultBanner.tsx +++ b/packages/react/src/auto/shadcn/submit/ShadcnSubmitResultBanner.tsx @@ -1,9 +1,8 @@ import React from "react"; -import type { ShadcnElements } from "../elements.js"; import { useResultBannerController } from "../../hooks/useResultBannerController.js"; +import type { ShadcnElements } from "../elements.js"; export const makeSubmitResultBanner = >({ Alert, AlertTitle, AlertDescription }: Elements) => { - if (!Alert || !AlertTitle || !AlertDescription) { throw new Error("Alert components are required"); } @@ -18,16 +17,16 @@ export const makeSubmitResultBanner = > return ( Success - - {title || "Operation completed successfully"} - - + {title || "Operation completed successfully"} + ); }; const ShadcnSubmitErrorBanner = (props: any) => { - const { show, successful, hide, title} = useResultBannerController(); + const { show, successful, hide, title } = useResultBannerController(); if (!show || successful) { return null; @@ -35,10 +34,10 @@ export const makeSubmitResultBanner = > return ( Error - - {title || "An error occurred"} - - + {title || "An error occurred"} + ); }; From d31871f11b7753a5f70fe337d7fc5cacf6e6ff91 Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Tue, 21 Jan 2025 16:00:51 -0500 Subject: [PATCH 05/14] Shadcn packages installation --- pnpm-lock.yaml | 240 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 223 insertions(+), 17 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb5a8d4bf..50b7354ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,6 +302,15 @@ importers: '@pollyjs/persister-fs': specifier: ^6.0.6 version: 6.0.6 + '@radix-ui/react-checkbox': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-label': + specifier: ^2.1.1 + version: 2.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-select': + specifier: ^2.1.4 + version: 2.1.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-toast': specifier: ^1.2.4 version: 1.2.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) @@ -5404,7 +5413,7 @@ packages: '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-icons': 1.3.0(react@18.2.0) '@radix-ui/react-popover': 1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-select': 2.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-select': 2.1.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-toggle-group': 1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-toolbar': 1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-tooltip': 1.1.2(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) @@ -5857,6 +5866,53 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@radix-ui/react-arrow@1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + + /@radix-ui/react-checkbox@1.1.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@radix-ui/react-collection@1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} peerDependencies: @@ -6130,6 +6186,19 @@ packages: react: 18.2.0 dev: true + /@radix-ui/react-focus-guards@1.1.1(@types/react@18.2.79)(react@18.2.0): + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.79 + react: 18.2.0 + dev: true + /@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} peerDependencies: @@ -6175,6 +6244,28 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@radix-ui/react-focus-scope@1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@radix-ui/react-icons@1.3.0(react@18.2.0): resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} peerDependencies: @@ -6212,6 +6303,26 @@ packages: react: 18.2.0 dev: true + /@radix-ui/react-label@2.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@radix-ui/react-popover@1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==} peerDependencies: @@ -6275,6 +6386,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@radix-ui/react-popper@1.2.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@floating-ui/react-dom': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-arrow': 1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/rect': 1.1.0 + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} peerDependencies: @@ -6491,8 +6631,8 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-select@2.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==} + /@radix-ui/react-select@2.1.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -6505,30 +6645,30 @@ packages: optional: true dependencies: '@radix-ui/number': 1.1.0 - '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.79)(react@18.2.0) - '@radix-ui/react-context': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.79)(react@18.2.0) '@radix-ui/react-direction': 1.1.0(@types/react@18.2.79)(react@18.2.0) - '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.2.79)(react@18.2.0) - '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-id': 1.1.0(@types/react@18.2.79)(react@18.2.0) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.1.0(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.1.1(@types/react@18.2.79)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.79)(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.79)(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.79)(react@18.2.0) '@radix-ui/react-use-previous': 1.1.0(@types/react@18.2.79)(react@18.2.0) - '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.79 '@types/react-dom': 18.2.25 aria-hidden: 1.2.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.7(@types/react@18.2.79)(react@18.2.0) + react-remove-scroll: 2.6.2(@types/react@18.2.79)(react@18.2.0) dev: true /@radix-ui/react-separator@1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): @@ -7609,7 +7749,7 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta dependencies: '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.79)(react@18.2.0) + '@radix-ui/react-slot': 1.1.1(@types/react@18.2.79)(react@18.2.0) '@storybook/client-logger': 8.1.6 '@storybook/csf': 0.1.8 '@storybook/global': 5.0.0 @@ -16847,6 +16987,22 @@ packages: tslib: 2.6.2 dev: true + /react-remove-scroll-bar@2.3.8(@types/react@18.2.79)(react@18.2.0): + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.79 + react: 18.2.0 + react-style-singleton: 2.2.3(@types/react@18.2.79)(react@18.2.0) + tslib: 2.6.2 + dev: true + /react-remove-scroll@2.5.5(@types/react@18.2.79)(react@18.2.0): resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} engines: {node: '>=10'} @@ -16885,6 +17041,25 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.79)(react@18.2.0) dev: true + /react-remove-scroll@2.6.2(@types/react@18.2.79)(react@18.2.0): + resolution: {integrity: sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.79 + react: 18.2.0 + react-remove-scroll-bar: 2.3.8(@types/react@18.2.79)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.79)(react@18.2.0) + tslib: 2.6.2 + use-callback-ref: 1.3.3(@types/react@18.2.79)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.79)(react@18.2.0) + dev: true + /react-style-singleton@2.2.1(@types/react@18.2.79)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -16902,6 +17077,22 @@ packages: tslib: 2.6.2 dev: true + /react-style-singleton@2.2.3(@types/react@18.2.79)(react@18.2.0): + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.79 + get-nonce: 1.0.1 + react: 18.2.0 + tslib: 2.6.2 + dev: true + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -18718,6 +18909,21 @@ packages: tslib: 2.6.2 dev: true + /use-callback-ref@1.3.3(@types/react@18.2.79)(react@18.2.0): + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.79 + react: 18.2.0 + tslib: 2.6.2 + dev: true + /use-sidecar@1.1.2(@types/react@18.2.79)(react@18.2.0): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} From 47527795e37859359558c26ee68d6f80d0bf4acb Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Tue, 21 Jan 2025 16:15:24 -0500 Subject: [PATCH 06/14] Add @gadgetinc/react to dependency --- packages/react/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/package.json b/packages/react/package.json index 9f5db0c87..bfcfd7fa3 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -49,6 +49,7 @@ "dependencies": { "@0no-co/graphql.web": "^1.0.4", "@gadgetinc/api-client-core": "^0.15.36", + "@gadgetinc/react": "workspace:*", "@hookform/resolvers": "^3.3.1", "filesize": "^10.1.2", "pluralize": "^8.0.0", From 69fa07a39b4a64fdc203a6af2142d4b0d6d50e31 Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Tue, 21 Jan 2025 16:23:24 -0500 Subject: [PATCH 07/14] Remove --- packages/react/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/package.json b/packages/react/package.json index a6ab6654b..23fc0bf13 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -48,7 +48,6 @@ }, "dependencies": { "@0no-co/graphql.web": "^1.0.4", - "@gadgetinc/react": "workspace:*", "@gadgetinc/api-client-core": "^0.15.38", "@hookform/resolvers": "^3.3.1", "filesize": "^10.1.2", From c3e186db87c5de8aae542a1d755d072ea94394d3 Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Tue, 21 Jan 2025 16:35:52 -0500 Subject: [PATCH 08/14] Update AutoFormMetadataContext props --- packages/react/src/auto/shadcn/ShadcnAutoForm.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx b/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx index 189392f9e..0b90abee6 100644 --- a/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx +++ b/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx @@ -74,10 +74,7 @@ const ShadcnAutoFormComponent = < apiIdentifier: action.modelApiIdentifier, namespace: action.namespace, }, - options: { - include: props.include, - exclude: props.exclude, - }, + fields, }; const formTitle = props.title === undefined ? humanizeCamelCase(action.operationName) : props.title; From ad47c5da474e2ecbf04669163d657fddb43de055 Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Tue, 21 Jan 2025 16:45:15 -0500 Subject: [PATCH 09/14] Install @gadgetinc/react workspace --- packages/react/package.json | 3 ++- packages/react/spec/auto/shadcn-defaults/index.tsx | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index 23fc0bf13..1ac32036e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -49,6 +49,7 @@ "dependencies": { "@0no-co/graphql.web": "^1.0.4", "@gadgetinc/api-client-core": "^0.15.38", + "@gadgetinc/react": "workspace:^", "@hookform/resolvers": "^3.3.1", "filesize": "^10.1.2", "pluralize": "^8.0.0", @@ -76,8 +77,8 @@ "@pollyjs/persister-fs": "^6.0.6", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-select": "^2.1.4", + "@radix-ui/react-toast": "^1.2.4", "@shopify/polaris": "^12.0.0", "@shopify/polaris-icons": "^8.1.0", "@storybook/addon-essentials": "^8.1.6", diff --git a/packages/react/spec/auto/shadcn-defaults/index.tsx b/packages/react/spec/auto/shadcn-defaults/index.tsx index 98ee4dbf6..666f29bc0 100644 --- a/packages/react/spec/auto/shadcn-defaults/index.tsx +++ b/packages/react/spec/auto/shadcn-defaults/index.tsx @@ -4,7 +4,7 @@ import { Button } from "./components/Button.js"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./components/Card.js"; import { Checkbox } from "./components/Checkbox.js"; import { Form } from "./components/Form.js"; -import { Select } from "./components/Select.js"; +// import { Select } from "./components/Select.js"; import { Input } from "./components/Input.js"; import { Label } from "./components/Label.js"; import { Skeleton } from "./components/Skeleton.js"; @@ -25,9 +25,8 @@ export const elements: ShadcnElements = { Form, Input, Label, - Select, Skeleton, - toast, + toast: toast as any, }; export { Toaster } from "./components/Toaster.js"; From 5980ebe4eb3cca630f43bf656621f39983969a2a Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Tue, 21 Jan 2025 17:04:08 -0500 Subject: [PATCH 10/14] Lint files and update lockfile --- .../cypress/component/auto/AutoButton.cy.tsx | 2 +- .../component/auto/form/AutoForm.cy.tsx | 1 - .../AutoFormDynamicFormInputChanges.cy.tsx | 8 +- .../auto/form/AutoFormUpsertAction.cy.tsx | 4 +- .../auto/form/AutoPasswordInput.cy.tsx | 1 - packages/react/cypress/support/auto.tsx | 31 +++-- packages/react/cypress/support/commands.ts | 2 +- packages/react/postcss.config.js | 2 +- .../auto/shadcn-defaults/components/Card.tsx | 112 +++++------------ .../shadcn-defaults/components/Select.tsx | 116 +++++++----------- .../react/src/auto/hooks/useRelatedModel.tsx | 3 +- .../auto/hooks/useResultBannerController.tsx | 3 +- .../react/src/auto/shadcn/ShadcnAutoForm.tsx | 5 +- packages/react/src/auto/shadcn/elements.tsx | 12 +- .../inputs/ShadcnAutoEncryptedStringInput.tsx | 4 +- .../shadcn/inputs/ShadcnAutoTextInput.tsx | 6 +- pnpm-lock.yaml | 3 + 17 files changed, 120 insertions(+), 195 deletions(-) diff --git a/packages/react/cypress/component/auto/AutoButton.cy.tsx b/packages/react/cypress/component/auto/AutoButton.cy.tsx index cefb8f73c..c6da559e6 100644 --- a/packages/react/cypress/component/auto/AutoButton.cy.tsx +++ b/packages/react/cypress/component/auto/AutoButton.cy.tsx @@ -3,7 +3,7 @@ import React from "react"; import { api } from "../../support/api.js"; import { describeForEachAutoAdapter } from "../../support/auto.js"; -describeForEachAutoAdapter("AutoButton", ({ name, adapter: { AutoButton }, wrapper }) => { +describeForEachAutoAdapter("AutoButton", ({ adapter: { AutoButton }, wrapper }) => { beforeEach(() => { cy.viewport("macbook-13"); }); diff --git a/packages/react/cypress/component/auto/form/AutoForm.cy.tsx b/packages/react/cypress/component/auto/form/AutoForm.cy.tsx index 4cbcca278..6056a1b65 100644 --- a/packages/react/cypress/component/auto/form/AutoForm.cy.tsx +++ b/packages/react/cypress/component/auto/form/AutoForm.cy.tsx @@ -129,7 +129,6 @@ describeForEachAutoAdapter("AutoForm", ({ name, adapter: { AutoForm }, wrapper } }); it("can render a form to update model and submit it", () => { - /** * This test is disabled for Shadcn because it's not supported yet. We need to have a list component for this to work properly */ diff --git a/packages/react/cypress/component/auto/form/AutoFormDynamicFormInputChanges.cy.tsx b/packages/react/cypress/component/auto/form/AutoFormDynamicFormInputChanges.cy.tsx index 3d5a59522..d9716f579 100644 --- a/packages/react/cypress/component/auto/form/AutoFormDynamicFormInputChanges.cy.tsx +++ b/packages/react/cypress/component/auto/form/AutoFormDynamicFormInputChanges.cy.tsx @@ -10,10 +10,10 @@ describeForEachAutoAdapter("AutoForm", ({ name, adapter: { AutoForm }, wrapper } return ( <> - +
-
); @@ -60,8 +60,6 @@ describeForEachAutoAdapter("AutoForm", ({ name, adapter: { AutoForm }, wrapper } cy.clickAndType(`input[name="widget.name"]`, "Dirty the value"); - - cy.clickAndType(`input[name="widget.inventoryCount"]`, "123546"); cy.get("#setFindById2").click(); diff --git a/packages/react/cypress/component/auto/form/AutoFormUpsertAction.cy.tsx b/packages/react/cypress/component/auto/form/AutoFormUpsertAction.cy.tsx index 144ee8778..22978e475 100644 --- a/packages/react/cypress/component/auto/form/AutoFormUpsertAction.cy.tsx +++ b/packages/react/cypress/component/auto/form/AutoFormUpsertAction.cy.tsx @@ -94,11 +94,11 @@ describeForEachAutoAdapter("AutoForm - Upsert Action", ({ name, adapter: { AutoF // Does not allow submission when the ID input does not have a positive integer value cy.clickAndType(`input[name="widget.id"]`, "-1", true); - + if (upsertHasBeenCalled) throw new Error("Upsert was called when it shouldn't have been"); cy.clickAndType(`input[name="widget.id"]`, "1.1", true); - + if (upsertHasBeenCalled) throw new Error("Upsert was called when it shouldn't have been"); cy.get(`input[name="widget.id"]`).clear().type("1{enter}"); diff --git a/packages/react/cypress/component/auto/form/AutoPasswordInput.cy.tsx b/packages/react/cypress/component/auto/form/AutoPasswordInput.cy.tsx index 20bb07b05..a2dbb24d9 100644 --- a/packages/react/cypress/component/auto/form/AutoPasswordInput.cy.tsx +++ b/packages/react/cypress/component/auto/form/AutoPasswordInput.cy.tsx @@ -58,7 +58,6 @@ describeForEachAutoAdapter("AutoPasswordInput", ({ name, adapter: { AutoForm }, cy.get(`input[name="user.password"]`).should("be.enabled"); cy.clickAndType(`input[name="user.password"]`, updatedPassword); - expectUpdateActionSubmissionVariables(expectedVariables); // Password field is changed and included submit("User", expectedVariables); diff --git a/packages/react/cypress/support/auto.tsx b/packages/react/cypress/support/auto.tsx index c3199f7e2..0e2ca35ef 100644 --- a/packages/react/cypress/support/auto.tsx +++ b/packages/react/cypress/support/auto.tsx @@ -16,12 +16,13 @@ interface AutoSuiteConfig { } const ONLY_RUN_SUITES = { - "Shadcn": ["AutoForm", - "AutoButton", - "AutoForm - input labels", - "AutoForm - Default model field values", - "AutoForm - FindBy object parameters", - "AutoForm - Global actions", + Shadcn: [ + "AutoForm", + "AutoButton", + "AutoForm - input labels", + "AutoForm - Default model field values", + "AutoForm - FindBy object parameters", + "AutoForm - Global actions", "AutoForm - HasManyThrough fields", "AutoForm - Dynamic form input changes", "AutoForm - Dynamic form input changes - FindBy object parameters", @@ -30,8 +31,8 @@ const ONLY_RUN_SUITES = { "AutoForm titles", "AutoForm - ID field", "AutoForm - Upsert Action", - "AutoPasswordInput" - ] + "AutoPasswordInput", + ], }; export const PolarisWrapper = ({ children }: { children: ReactNode }) => ( @@ -49,11 +50,9 @@ const ShadCNAdapter = makeAutocomponents({ ...elements }); export const ShadcnWrapper = ({ children }: { children: ReactNode }) => ( <> - - - - {children} - + + + {children} @@ -67,9 +66,7 @@ const suites: AutoSuiteConfig[] = [ export const adapters = [PolarisAdapter]; export const describeForEachAutoAdapter = (suiteName: string, suite: (config: AutoSuiteConfig) => void) => { - const filteredSuites = suites.filter(config => - config.name !== "Shadcn" || ONLY_RUN_SUITES["Shadcn"].includes(suiteName) - ); - + const filteredSuites = suites.filter((config) => config.name !== "Shadcn" || ONLY_RUN_SUITES["Shadcn"].includes(suiteName)); + // eslint-disable-next-line jest/valid-describe-callback, jest/valid-title describe.each(filteredSuites)((({ name }: { name: string }) => `${suiteName} - ${name}`) as any, suite); }; diff --git a/packages/react/cypress/support/commands.ts b/packages/react/cypress/support/commands.ts index ed633ae71..11e7eeab8 100644 --- a/packages/react/cypress/support/commands.ts +++ b/packages/react/cypress/support/commands.ts @@ -98,7 +98,7 @@ Cypress.Commands.add("getSubmitButton", () => { * Click on an element and type text into it * This command is used to simulate user interaction with the UI due to the fact that Cypress kept seeing some input elements as disabled * more on the issue here: https://github.com/cypress-io/cypress/issues/5830 and https://github.com/cypress-io/cypress/issues/5827 - * + * * @param selector - The selector of the element to click and type into * @param text - The text to type into the element * @param clear - Whether to clear the element before typing diff --git a/packages/react/postcss.config.js b/packages/react/postcss.config.js index 2e7af2b7f..2aa7205d4 100644 --- a/packages/react/postcss.config.js +++ b/packages/react/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/packages/react/spec/auto/shadcn-defaults/components/Card.tsx b/packages/react/spec/auto/shadcn-defaults/components/Card.tsx index a2caefc28..7ef18135a 100644 --- a/packages/react/spec/auto/shadcn-defaults/components/Card.tsx +++ b/packages/react/spec/auto/shadcn-defaults/components/Card.tsx @@ -1,79 +1,35 @@ -import * as React from "react" - -import { cn } from "../utils.js" - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -Card.displayName = "Card" - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardHeader.displayName = "CardHeader" - -const CardTitle = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardTitle.displayName = "CardTitle" - -const CardDescription = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardDescription.displayName = "CardDescription" - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( +import * as React from "react"; + +import { cn } from "../utils.js"; + +const Card = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef>(({ className, ...props }, ref) => (
-)) -CardContent.displayName = "CardContent" - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardFooter.displayName = "CardFooter" - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } \ No newline at end of file +)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; diff --git a/packages/react/spec/auto/shadcn-defaults/components/Select.tsx b/packages/react/spec/auto/shadcn-defaults/components/Select.tsx index c119643dd..a8f74c6da 100644 --- a/packages/react/spec/auto/shadcn-defaults/components/Select.tsx +++ b/packages/react/spec/auto/shadcn-defaults/components/Select.tsx @@ -1,15 +1,15 @@ -import * as React from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import { Check, ChevronDown, ChevronUp } from "lucide-react" - -import { cn } from "../utils.js" - -const Select = SelectPrimitive.Root - -const SelectGroup = SelectPrimitive.Group - -const SelectValue = SelectPrimitive.Value - +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; +import * as React from "react"; + +import { cn } from "../utils.js"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + const SelectTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -27,44 +27,29 @@ const SelectTrigger = React.forwardRef< -)) -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName - +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + const SelectScrollUpButton = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + -)) -SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName - +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + const SelectScrollDownButton = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + -)) -SelectScrollDownButton.displayName = - SelectPrimitive.ScrollDownButton.displayName - +)); +SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; + const SelectContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -85,8 +70,7 @@ const SelectContent = React.forwardRef< {children} @@ -94,21 +78,17 @@ const SelectContent = React.forwardRef< -)) -SelectContent.displayName = SelectPrimitive.Content.displayName - +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + const SelectLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -SelectLabel.displayName = SelectPrimitive.Label.displayName - + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + const SelectItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -126,33 +106,29 @@ const SelectItem = React.forwardRef< - + {children} -)) -SelectItem.displayName = SelectPrimitive.Item.displayName - +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + const SelectSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -SelectSeparator.displayName = SelectPrimitive.Separator.displayName - + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + export { Select, - SelectGroup, - SelectValue, - SelectTrigger, SelectContent, - SelectLabel, + SelectGroup, SelectItem, - SelectSeparator, - SelectScrollUpButton, + SelectLabel, SelectScrollDownButton, -} \ No newline at end of file + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/packages/react/src/auto/hooks/useRelatedModel.tsx b/packages/react/src/auto/hooks/useRelatedModel.tsx index 616fbb5d1..5f444cf53 100644 --- a/packages/react/src/auto/hooks/useRelatedModel.tsx +++ b/packages/react/src/auto/hooks/useRelatedModel.tsx @@ -1,5 +1,6 @@ import { assert, type FieldSelection } from "@gadgetinc/api-client-core"; -import React, { useCallback, useEffect, useState } from "react"; +import type React from "react"; +import { useCallback, useEffect, useState } from "react"; import { FieldType } from "../../metadata.js"; import { type RecordIdentifier } from "../../use-action-form/types.js"; import { useFindMany } from "../../useFindMany.js"; diff --git a/packages/react/src/auto/hooks/useResultBannerController.tsx b/packages/react/src/auto/hooks/useResultBannerController.tsx index e0ccdff52..61c719457 100644 --- a/packages/react/src/auto/hooks/useResultBannerController.tsx +++ b/packages/react/src/auto/hooks/useResultBannerController.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { isModelActionMetadata } from "../../metadata.js"; -import { AutoFormSubmitResult, useAutoFormMetadata } from "../AutoFormContext.js"; +import type { AutoFormSubmitResult } from "../AutoFormContext.js"; +import { useAutoFormMetadata } from "../AutoFormContext.js"; export const useResultBannerController = () => { const { metadata, submitResult } = useAutoFormMetadata(); diff --git a/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx b/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx index 0b90abee6..3a8d99e6c 100644 --- a/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx +++ b/packages/react/src/auto/shadcn/ShadcnAutoForm.tsx @@ -1,5 +1,6 @@ import type { ActionFunction } from "@gadgetinc/api-client-core"; -import React, { ComponentProps } from "react"; +import type { ComponentProps } from "react"; +import React from "react"; import { FormProvider } from "../../useActionForm.js"; import { humanizeCamelCase, type OptionsType } from "../../utils.js"; import type { AutoFormProps } from "../AutoForm.js"; @@ -99,7 +100,7 @@ const ShadcnAutoFormComponent = < {!metadataError && ( <> {fields.map(({ metadata }) => ( - + ))} {props.submitLabel ?? "Submit"} diff --git a/packages/react/src/auto/shadcn/elements.tsx b/packages/react/src/auto/shadcn/elements.tsx index c943e98f5..af2e883b9 100644 --- a/packages/react/src/auto/shadcn/elements.tsx +++ b/packages/react/src/auto/shadcn/elements.tsx @@ -1,4 +1,4 @@ -import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import type * as CheckboxPrimitive from "@radix-ui/react-checkbox"; import type React from "react"; import type { PropsWithChildren } from "react"; @@ -15,7 +15,7 @@ export interface ButtonProps extends BaseProps, React.ButtonHTMLAttributes {} +export type SkeletonProps = Pick; /** The props that an alert component injected into autocomponent's shadcn must support */ export interface AlertProps extends Omit { @@ -23,19 +23,19 @@ export interface AlertProps extends Omit { } /** The props that an alert title component injected into autocomponent's shadcn must support */ -export interface AlertTitleProps extends Pick {} +export type AlertTitleProps = Pick; /** The props that an alert description component injected into autocomponent's shadcn must support */ -export interface AlertDescriptionProps extends Pick {} +export type AlertDescriptionProps = Pick; /** The props that a form component injected into autocomponent's shadcn must support */ -export interface FormProps extends Pick {} +export type FormProps = Pick; /** The props that an input component injected into autocomponent's shadcn must support */ export interface InputProps extends Pick, React.InputHTMLAttributes {} /** The props that a label component injected into autocomponent's shadcn must support */ -export interface LabelProps extends React.LabelHTMLAttributes {} +export type LabelProps = React.LabelHTMLAttributes; /** The props that a checkbox component injected into autocomponent's shadcn must support */ export interface CheckboxProps extends Omit, "type"> { diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoEncryptedStringInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoEncryptedStringInput.tsx index 503d1a97a..a939e97cd 100644 --- a/packages/react/src/auto/shadcn/inputs/ShadcnAutoEncryptedStringInput.tsx +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoEncryptedStringInput.tsx @@ -25,8 +25,6 @@ export const makeShadcnAutoEncryptedStringInput = ({ Input, Label, Button }: Pic ); - return ( - - ); + return ; }); }; diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx index 5fd1a5fee..1fce78fe4 100644 --- a/packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoTextInput.tsx @@ -38,11 +38,7 @@ export const makeShadcnAutoTextInput = ({ Input, Label }: Pick - {suffix && ( -
- {suffix} -
- )} + {suffix &&
{suffix}
}
{stringInputController.errorMessage &&

{stringInputController.errorMessage}

}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc687286a..30c5bde07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,6 +235,9 @@ importers: '@gadgetinc/api-client-core': specifier: workspace:* version: link:../api-client-core + '@gadgetinc/react': + specifier: workspace:* + version: 'link:' '@hookform/resolvers': specifier: ^3.3.1 version: 3.6.0(react-hook-form@7.48.2) From a2b4b7345e66a7714b58875fdb13a4b1f641aa35 Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Tue, 21 Jan 2025 17:43:57 -0500 Subject: [PATCH 11/14] remove circular dependency --- packages/react/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/package.json b/packages/react/package.json index 1ac32036e..4bb8794ce 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -49,7 +49,6 @@ "dependencies": { "@0no-co/graphql.web": "^1.0.4", "@gadgetinc/api-client-core": "^0.15.38", - "@gadgetinc/react": "workspace:^", "@hookform/resolvers": "^3.3.1", "filesize": "^10.1.2", "pluralize": "^8.0.0", From a630362415dc0541d568f7e6e158af283c67d9e6 Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Tue, 21 Jan 2025 17:47:21 -0500 Subject: [PATCH 12/14] Update lockfile to sync with package.json --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30c5bde07..dc687286a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,9 +235,6 @@ importers: '@gadgetinc/api-client-core': specifier: workspace:* version: link:../api-client-core - '@gadgetinc/react': - specifier: workspace:* - version: 'link:' '@hookform/resolvers': specifier: ^3.3.1 version: 3.6.0(react-hook-form@7.48.2) From 2db79f88f3ec80a524a8712df0dc06c65569d003 Mon Sep 17 00:00:00 2001 From: Jolaade Adewale Date: Tue, 21 Jan 2025 18:38:31 -0500 Subject: [PATCH 13/14] remove reference from spec in src --- .../react/src/auto/shadcn/inputs/ShadcnAutoBooleanInput.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react/src/auto/shadcn/inputs/ShadcnAutoBooleanInput.tsx b/packages/react/src/auto/shadcn/inputs/ShadcnAutoBooleanInput.tsx index 434c28216..8f17b677c 100644 --- a/packages/react/src/auto/shadcn/inputs/ShadcnAutoBooleanInput.tsx +++ b/packages/react/src/auto/shadcn/inputs/ShadcnAutoBooleanInput.tsx @@ -4,8 +4,6 @@ import { useController, useFormContext } from "../../../useActionForm.js"; import { get } from "../../../utils.js"; import { autoInput } from "../../AutoInput.js"; import { useFieldMetadata } from "../../hooks/useFieldMetadata.js"; -// should we move this to a shared location? -import { cn } from "../../../../spec/auto/shadcn-defaults/utils.js"; import type { CheckboxProps, ShadcnElements } from "../elements.js"; export const makeShadcnAutoBooleanInput = ({ Checkbox, Label }: Pick) => { @@ -48,7 +46,7 @@ export const makeShadcnAutoBooleanInput = ({ Checkbox, Label }: Pick -