Skip to content

Commit

Permalink
feat: add model-properties form component
Browse files Browse the repository at this point in the history
  • Loading branch information
belafonte committed Oct 22, 2024
1 parent 83c77d7 commit 02cd1ec
Show file tree
Hide file tree
Showing 11 changed files with 440 additions and 199 deletions.
57 changes: 57 additions & 0 deletions src/leihs/inventory/client/components/basic_form_field.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
(ns leihs.inventory.client.components.basic-form-field
(:require
["@@/checkbox" :refer [Checkbox]]
["@@/dropzone" :refer [Dropzone]]
["@@/form" :refer [FormField FormItem FormLabel FormControl FormDescription FormMessage]]
["@@/input" :refer [Input]]
["@@/textarea" :refer [Textarea]]
[leihs.inventory.client.lib.utils :refer [cj jc]]
[leihs.inventory.client.routes.models.create.components.accessories-list :refer [AccessoryList]]
[uix.core :as uix :refer [defui $]]))

comment "available form fields"
(def fields-map
{"input" Input
"dropzone" Dropzone
"textarea" Textarea
"checkbox" Checkbox
"accessory-list" AccessoryList})

(defui main
"Main function for rendering a form field component.
Arguments:
- `control`: The control element for the form field.
- `input`: A map containing input properties.
- `class-name`: A string for additional CSS class names for the input field
- `label`: A boolean indicating whether to display the label.
- `description`: A boolean indicating whether to display the description.
- `name`: The name of the form field.
Default values:
- `label`: true
- `description`: true
- `class-name`: empty string
- `name`: The name from the `input` map."

[{:keys [control input class-name
label description name]
:or {label true description true
class-name "" name (:name input)}}]

(let [comp (get fields-map (:component input))]
(when comp
($ FormField {:control (cj control)
:name name
:render #($ FormItem
(when label ($ FormLabel (:label input)))
($ FormControl
($ comp (merge
{:class-name class-name}
(:props input)
(:field (jc %)))))

(when description ($ FormDescription
($ :<> (:description input))))

($ FormMessage))}))))
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef, useState } from "react"
import React, { useState } from "react"

// Create the context
const ScrollspyContext = React.createContext()
Expand All @@ -10,9 +10,12 @@ export const ScrollspyProvider = ({ children }) => {

// Function to add an item
function addItem(item) {
if (items.find((i) => i.id === item.id)) return

setItems((prev) => [...prev, item])
setItems((prev) => {
if (prev.find((i) => i.id === item.id)) {
return [...prev]
}
return [...prev, item]
})
}

const value = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { cn } from "@@/utils"
export function Scrollspy({ children, className }) {
return (
<ScrollspyProvider>
<div className={className + " scroll-smooth"}>{children}</div>
<div className={className + " scroll-smooth flex flex-col lg:flex-row"}>
{children}
</div>
</ScrollspyProvider>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,18 @@ function SortableList({ children, items, onDragEnd, setItems = false }) {
)
}

function Draggable({ children, className, id, ...props }) {
function Draggable({ children, className, id, asChild = false, ...props }) {
const { transition, transform, setNodeRef } = useSortable({ id })
const Comp = asChild ? Slot : "div"

const style = {
transform: CSS.Transform.toString(transform),
transition,
}
return (
<div style={style} {...props} ref={setNodeRef}>
<Comp style={style} {...props} ref={setNodeRef}>
{children}
</div>
</Comp>
)
}

Expand Down
22 changes: 21 additions & 1 deletion src/leihs/inventory/client/components/ui/form.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { Controller, FormProvider, useFormContext } from "react-hook-form"
import {
Controller,
FormProvider,
useFormContext,
useFieldArray,
} from "react-hook-form"

import { cn } from "@/components/ui/utils"
import { Label } from "@/components/ui/label"
Expand All @@ -9,6 +14,21 @@ const Form = FormProvider

const FormFieldContext = React.createContext({})

// const FormFields = ({ ...props }) => {
// const control = props.control;
//
// const { fields, append, remove } = useFieldArray({
// control,
// name: ""
// });
//
// return (
// <FormFieldContext.Provider value={{ name: props.name }}>
// <Controller name={} {...props} />
// </FormFieldContext.Provider>
// )
// }

const FormField = ({ ...props }) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
Expand Down
67 changes: 55 additions & 12 deletions src/leihs/inventory/client/components/ui/textarea.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,61 @@ import * as React from "react"

import { cn } from "@/components/ui/utils"

const Textarea = React.forwardRef(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
)
})
const Textarea = React.forwardRef(
(
{ className, autoscale = false, resize = true, onBlur, onFocus, ...props },
ref,
) => {
const [isFocused, setIsFocused] = React.useState(false)
const textareaRef = React.useRef(null)

React.useImperativeHandle(ref, () => textareaRef.current)

const adjustHeight = () => {
const textarea = textareaRef.current
if (textarea) {
if (isFocused) {
textarea.style.height = "auto"
textarea.style.height = `${textarea.scrollHeight}px`
} else {
textarea.style.height = "2.5rem" // Set to 1 line height when not focused
}
}
}

React.useEffect(() => {
adjustHeight()
}, [props.value, isFocused])

function handleBlur(e) {
autoscale && setIsFocused(false)
onBlur && onBlur(e)
}

function handleFocus(e) {
autoscale && setIsFocused(true)
onFocus && onFocus(e)
}

return (
<textarea
ref={textareaRef}
style={{
height: autoscale ? textareaRef.current?.scrollHeight : undefined,
}}
onFocus={(e) => handleFocus(e)}
onBlur={(e) => handleBlur(e)}
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
autoscale && "overflow-hidden transition-all duration-200",
!props.resize && "resize-none",
className,
)}
{...props}
/>
)
},
)
Textarea.displayName = "Textarea"

export { Textarea }
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
:on-change #(set-accessory! (.. % -target -value))
:on-blur (:onBlur props)
:aria-invalid (:aria-invalid props)
:aria-describedby (::aria-describedby props)})
:aria-describedby (:aria-describedby props)})

($ Button {:type "button"
:className ""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
(ns leihs.inventory.client.routes.models.create.components.model-properties
(:require
["@/components/react/sortable-list" :refer [SortableList Draggable DragHandle]]
["@@/button" :refer [Button]]
["@@/form" :refer [FormField FormItem FormControl FormMessage]]
["@@/table" :refer [Table TableHeader TableRow TableHead TableBody TableCell]]
["@@/textarea" :refer [Textarea]]
["lucide-react" :refer [CirclePlus Trash]]
["react-hook-form" :as hook-form]
[leihs.inventory.client.lib.utils :refer [cj jc]]
[uix.core :as uix :refer [defui $]]
[uix.dom]))

(defn find-index-by-id [vec id]
(some (fn [[idx item]]
(when (= (:id item) id)
idx))
(map-indexed vector vec)))

(defn handle-drag-end [event fields move]
(let [ev (jc event)]
(when-not (= (-> ev :over :id)
(-> ev :active :id))

(let [old-index (find-index-by-id fields
(-> ev :active :id))
new-index (find-index-by-id fields
(-> ev :over :id))]

(move old-index new-index)))))

(defui properties-table [{:keys [children inputs]}]
($ Table
($ TableHeader
($ TableRow
($ TableHead (-> inputs (nth 0) :label))
($ TableHead (-> inputs (nth 1) :label))
($ TableHead "Actions")))
($ TableBody children)))

(defui main [{:keys [control props]}]
(let [{:keys [fields append remove move]} (jc (hook-form/useFieldArray
(cj {:control control
:name "properties"})))
inputs (:inputs props)]

($ :div {:className "flex flex-col gap-2"}

(when (not-empty fields)
($ :div {:className "rounded-md border"}

($ SortableList {:items (cj (map :id fields))
:onDragEnd (fn [e] (handle-drag-end e fields move))}
($ properties-table {:inputs inputs}
(doall
(map-indexed
(fn [index field]
($ Draggable {:key (:id field)
:id (:id field)
:asChild true}

($ TableRow {:key (:id field)}

($ TableCell
($ FormField
{:control (cj control)
:name (str "properties." index (-> inputs (nth 0) :name))
:render #($ FormItem
($ FormControl
($ Textarea (merge
{:className "min-h-[2.5rem]"}
(-> inputs (nth 0) :props)
(:field (jc %))))))}

($ FormMessage)))

($ TableCell
($ FormField
{:control (cj control)
:name (str "properties." index (-> inputs (nth 1) :name))
:render #($ FormItem
($ FormControl
($ Textarea (merge
{:className "min-h-[2.5rem]"}
(-> inputs (nth 1) :props)
(:field (jc %))))))}

($ FormMessage)))

($ TableCell
($ :div {:className "ml-auto flex gap-2"}
($ DragHandle {:id (:id field)
:className "cursor-move"})

($ Button {:variant "outline"
:size "icon"
:className "cursor-pointer"
:on-click #(remove index)}
($ Trash {:className "p-1"})))))))
fields))))))

($ :div {:className "flex"}
($ Button {:type "button"
:className ""
:variant "outline"
:on-click #(append (cj {:name "" :value ""}))}

($ CirclePlus {:className "p-1"}) "Eigenschaft hinzufügen")))))

(def ModelProperties
(uix/as-react
(fn [props]
(main props))))
Loading

0 comments on commit 02cd1ec

Please sign in to comment.