Skip to content

Commit a98b26c

Browse files
committed
fix merging classes into components
1 parent 312d571 commit a98b26c

File tree

9 files changed

+168
-42
lines changed

9 files changed

+168
-42
lines changed

src/components/Alert.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { AlertProps } from '@/components/types'
2+
import React from 'react'
23

3-
export default function Alert({ type = "warn", hideIcon, className, children }: AlertProps) {
4+
export default function Alert({ type = "warn", hideIcon, className, children, ...attrs }: AlertProps & React.HTMLAttributes<HTMLDivElement>) {
45
const backgroundColor = type === "info"
56
? "bg-blue-50 dark:bg-blue-200"
67
: type === "error"
@@ -53,7 +54,7 @@ export default function Alert({ type = "warn", hideIcon, className, children }:
5354
</div>
5455
)}
5556
<div>
56-
<div className={`${textColor} text-sm`}>
57+
<div className={`${textColor} text-sm`} {...attrs}>
5758
{children}
5859
</div>
5960
</div>

src/components/AutoFormFields.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ const AutoFormFields = forwardRef<AutoFormFieldsRef, AutoFormFieldsProps>(({
3232

3333
const { metadataApi, apiOf, typeOf, typeOfRef, createFormLayout, Crud } = useMetadata()
3434

35-
const typeName = useMemo(() => getTypeName(typeProp || modelValue), [typeProp, modelValue])
35+
const typeName = useMemo(() => {
36+
// If formLayout is provided, we don't need a type
37+
if (formLayout && !typeProp) return null
38+
return getTypeName(typeProp || modelValue)
39+
}, [typeProp, modelValue, formLayout])
3640

3741
const type = useMemo(() => metaType ?? typeOf(typeName), [metaType, typeName, typeOf])
3842

src/components/Autocomplete.tsx

Lines changed: 123 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const Autocomplete = forwardRef<AutocompleteRef, AutocompleteProps & Omit<React.
3434
status,
3535
onChange,
3636
children,
37+
className,
3738
...attrs
3839
}, ref) => {
3940
const [expanded, setExpanded] = useState(false)
@@ -42,6 +43,7 @@ const Autocomplete = forwardRef<AutocompleteRef, AutocompleteProps & Omit<React.
4243
const [take, setTake] = useState(viewCount)
4344
const [filteredValues, setFilteredValues] = useState<any[]>([])
4445
const txtInputRef = useRef<HTMLInputElement>(null)
46+
const containerRef = useRef<HTMLDivElement>(null)
4547

4648
const apiState = useApiState()
4749
const useLabel = label ?? humanize(toPascalCase(id))
@@ -57,10 +59,20 @@ const Autocomplete = forwardRef<AutocompleteRef, AutocompleteProps & Omit<React.
5759
[responseStatus, id]
5860
)
5961

60-
const cls = useMemo(() => [
61-
input.base,
62-
errorField ? input.invalid : input.valid
63-
].join(' '), [errorField])
62+
const cls = useMemo(() => {
63+
if (multiple) {
64+
return [
65+
'w-full cursor-text flex flex-wrap sm:text-sm rounded-md dark:text-white dark:bg-gray-900 border focus-within:border-transparent focus-within:ring-1 focus-within:outline-none',
66+
errorField
67+
? 'pr-10 border-red-300 text-red-900 placeholder-red-300 focus-within:outline-none focus-within:ring-red-500 focus-within:border-red-500'
68+
: 'shadow-sm border-gray-300 dark:border-gray-600 focus-within:ring-indigo-500 focus-within:border-indigo-500'
69+
].join(' ')
70+
}
71+
return [
72+
input.base,
73+
errorField ? input.invalid : input.valid
74+
].join(' ')
75+
}, [errorField, multiple])
6476

6577
const filteredOptions = useMemo(() => {
6678
let ret = !inputValue
@@ -234,24 +246,53 @@ const Autocomplete = forwardRef<AutocompleteRef, AutocompleteProps & Omit<React.
234246
refresh()
235247
}, [])
236248

249+
// Close dropdown when clicking outside
250+
useEffect(() => {
251+
const handleClickOutside = (event: MouseEvent) => {
252+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
253+
setExpanded(false)
254+
}
255+
}
256+
257+
if (expanded) {
258+
document.addEventListener('mousedown', handleClickOutside)
259+
return () => {
260+
document.removeEventListener('mousedown', handleClickOutside)
261+
}
262+
}
263+
}, [expanded])
264+
237265
const hasOption = useCallback((option: any) => {
238-
return Array.isArray(value) && value.indexOf(option) >= 0
266+
if (!Array.isArray(value)) return false
267+
268+
// For objects, compare by key property if it exists
269+
if (option && typeof option === 'object' && 'key' in option) {
270+
return value.some(v => v && typeof v === 'object' && 'key' in v && v.key === option.key)
271+
}
272+
273+
// For primitives, use indexOf
274+
return value.indexOf(option) >= 0
239275
}, [value])
240276

241277
const select = useCallback((option: any) => {
242-
setInputValue('')
243-
setExpanded(false)
244-
245278
if (multiple) {
246279
let newValues = Array.from(value || [])
247280
if (hasOption(option)) {
248-
newValues = newValues.filter(x => x !== option)
281+
// Remove the option - use proper comparison for objects
282+
if (option && typeof option === 'object' && 'key' in option) {
283+
newValues = newValues.filter(x => !(x && typeof x === 'object' && 'key' in x && x.key === option.key))
284+
} else {
285+
newValues = newValues.filter(x => x !== option)
286+
}
249287
} else {
250288
newValues.push(option)
251289
}
252290
setActive(null)
253291
onChange?.(newValues)
292+
// Don't clear input or close dropdown in multiple mode - keep it open for more selections
254293
} else {
294+
setInputValue('')
295+
setExpanded(false)
255296
let val = option
256297
onChange?.(val)
257298
}
@@ -274,35 +315,86 @@ const Autocomplete = forwardRef<AutocompleteRef, AutocompleteProps & Omit<React.
274315
}, [children])
275316

276317
return (
277-
<div id={`${id}-autocomplete`}>
318+
<div id={`${id}-autocomplete`} className={className} ref={containerRef}>
278319
{useLabel && (
279320
<label htmlFor={`${id}-text`} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
280321
{useLabel}
281322
</label>
282323
)}
283324

284325
<div className="relative mt-1">
285-
<input
286-
ref={txtInputRef}
287-
id={`${id}-text`}
288-
type="text"
289-
role="combobox"
290-
aria-controls="options"
291-
aria-expanded="false"
292-
autoComplete="off"
293-
spellCheck="false"
294-
value={inputValue}
295-
onChange={(e) => setInputValue(e.target.value)}
296-
className={cls}
297-
placeholder={multiple || !value ? placeholder : ''}
298-
readOnly={!multiple && !!value && !expanded}
299-
onKeyDown={keyDown}
300-
onKeyUp={keyUp}
301-
onClick={onInputClick}
302-
onPaste={onPaste}
303-
required={false}
304-
{...attrs}
305-
/>
326+
{multiple ? (
327+
<div className={cls} onClick={onInputClick} tabIndex={-1}>
328+
<div className="flex flex-wrap pb-1.5">
329+
{Array.isArray(value) && value.map((option, idx) => (
330+
<div key={idx} className="pt-1.5 pl-1">
331+
<span className="inline-flex rounded-full items-center py-0.5 pl-2.5 pr-1 text-sm font-medium bg-indigo-100 dark:bg-indigo-800 text-indigo-700 dark:text-indigo-300">
332+
{renderItem(option)}
333+
<button
334+
type="button"
335+
onClick={(e) => {
336+
e.stopPropagation()
337+
if (Array.isArray(value)) {
338+
onChange?.(value.filter(v => v !== option))
339+
}
340+
}}
341+
className="flex-shrink-0 ml-1 h-4 w-4 rounded-full inline-flex items-center justify-center text-indigo-400 dark:text-indigo-500 hover:bg-indigo-200 dark:hover:bg-indigo-800 hover:text-indigo-500 dark:hover:text-indigo-400 focus:outline-none focus:bg-indigo-500 focus:text-white dark:focus:text-black"
342+
>
343+
<svg className="h-2 w-2" stroke="currentColor" fill="none" viewBox="0 0 8 8">
344+
<path strokeLinecap="round" strokeWidth="1.5" d="M1 1l6 6m0-6L1 7" />
345+
</svg>
346+
</button>
347+
</span>
348+
</div>
349+
))}
350+
<div className="pt-1.5 pl-1 shrink">
351+
<input
352+
ref={txtInputRef}
353+
id={`${id}-text`}
354+
type="text"
355+
role="combobox"
356+
aria-controls="options"
357+
aria-expanded="false"
358+
autoComplete="off"
359+
spellCheck="false"
360+
className="p-0 dark:bg-transparent rounded-md border-none focus:!border-none focus:!outline-none"
361+
style={{ boxShadow: 'none !important', width: `${inputValue.length + 1}ch` }}
362+
value={inputValue}
363+
onChange={(e) => setInputValue(e.target.value)}
364+
placeholder={!value || (Array.isArray(value) && value.length === 0) ? placeholder : ''}
365+
onKeyDown={keyDown}
366+
onKeyUp={keyUp}
367+
onClick={onInputClick}
368+
onPaste={onPaste}
369+
required={false}
370+
{...attrs}
371+
/>
372+
</div>
373+
</div>
374+
</div>
375+
) : (
376+
<input
377+
ref={txtInputRef}
378+
id={`${id}-text`}
379+
type="text"
380+
role="combobox"
381+
aria-controls="options"
382+
aria-expanded="false"
383+
autoComplete="off"
384+
spellCheck="false"
385+
value={inputValue}
386+
onChange={(e) => setInputValue(e.target.value)}
387+
className={cls}
388+
placeholder={multiple || !value ? placeholder : ''}
389+
readOnly={!multiple && !!value && !expanded}
390+
onKeyDown={keyDown}
391+
onKeyUp={keyUp}
392+
onClick={onInputClick}
393+
onPaste={onPaste}
394+
required={false}
395+
{...attrs}
396+
/>
397+
)}
306398

307399
<button
308400
type="button"

src/components/Combobox.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const Combobox = forwardRef<ComboboxRef, ComboboxProps & Omit<React.HTMLAttribut
1818
values,
1919
entries,
2020
onChange,
21+
label,
22+
help,
23+
placeholder,
24+
className,
2125
children,
2226
...attrs
2327
}, ref) => {
@@ -44,7 +48,14 @@ const Combobox = forwardRef<ComboboxRef, ComboboxProps & Omit<React.HTMLAttribut
4448
)
4549

4650
const updateModelValue = useCallback((newModel: any[] | any) => {
47-
onChange?.(newModel)
51+
// Convert Pair objects back to keys for the parent component
52+
if (Array.isArray(newModel)) {
53+
onChange?.(newModel.map(x => x.key))
54+
} else if (newModel && typeof newModel === 'object' && 'key' in newModel) {
55+
onChange?.(newModel.key)
56+
} else {
57+
onChange?.(newModel)
58+
}
4859
}, [onChange])
4960

5061
const update = useCallback(() => {
@@ -88,14 +99,18 @@ const Combobox = forwardRef<ComboboxRef, ComboboxProps & Omit<React.HTMLAttribut
8899
ref={inputRef}
89100
id={id}
90101
status={status}
102+
label={label}
103+
help={help}
104+
placeholder={placeholder}
105+
className={className}
91106
options={kvpValues}
92107
match={match}
93108
multiple={multiple}
94109
value={model}
95110
onChange={updateModelValue}
96111
{...attrs}
97112
>
98-
{({ key, value }: { key: string, value: string }) => (
113+
{({ value }: { key: string, value: string }) => (
99114
<span className="block truncate">{value}</span>
100115
)}
101116
</Autocomplete>

src/components/FileInput.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const FileInput: React.FC<FileInputProps & Omit<React.InputHTMLAttributes<HTMLIn
3030
files,
3131
status,
3232
onChange,
33+
className,
3334
...attrs
3435
}) => {
3536
const apiState = useApiState()
@@ -134,7 +135,7 @@ const FileInput: React.FC<FileInputProps & Omit<React.InputHTMLAttributes<HTMLIn
134135
}, [])
135136

136137
return (
137-
<div className={`flex ${!multiple ? 'justify-between' : 'flex-col'}`}>
138+
<div className={`flex ${!multiple ? 'justify-between' : 'flex-col'} ${className || ''}`}>
138139
<div className="relative flex-grow mr-2 sm:mr-4">
139140
{useLabel && (
140141
<label htmlFor={id} className={`block text-sm font-medium text-gray-700 dark:text-gray-300 ${labelClass ?? ''}`}>

src/components/OutlineButton.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { useMemo } from 'react'
22
import { Link } from 'react-router-dom'
33
import type { OutlineButtonProps } from '@/components/types'
44

@@ -7,9 +7,13 @@ const OutlineButton: React.FC<OutlineButtonProps> = ({
77
href,
88
onClick,
99
children,
10+
className,
1011
...attrs
1112
}) => {
12-
const cls = "inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm font-medium rounded-md text-gray-700 dark:text-gray-200 disabled:text-gray-400 bg-white dark:bg-black hover:bg-gray-50 hover:dark:bg-gray-900 disabled:hover:bg-white dark:disabled:hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:ring-offset-black"
13+
const cls = useMemo(() => {
14+
const baseClasses = "inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm font-medium rounded-md text-gray-700 dark:text-gray-200 disabled:text-gray-400 bg-white dark:bg-black hover:bg-gray-50 hover:dark:bg-gray-900 disabled:hover:bg-white dark:disabled:hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:ring-offset-black"
15+
return className ? `${baseClasses} ${className}` : baseClasses
16+
}, [className])
1317

1418
if (href) {
1519
return (

src/components/SecondaryButton.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { useMemo } from 'react'
22
import { Link } from 'react-router-dom'
33
import type { SecondaryButtonProps } from '@/components/types'
44

@@ -7,9 +7,13 @@ const SecondaryButton: React.FC<SecondaryButtonProps> = ({
77
href,
88
onClick,
99
children,
10+
className,
1011
...attrs
1112
}) => {
12-
const cls = "inline-flex justify-center rounded-md border border-gray-300 py-2 px-4 text-sm font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-400 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:ring-offset-black"
13+
const cls = useMemo(() => {
14+
const baseClasses = "inline-flex justify-center rounded-md border border-gray-300 py-2 px-4 text-sm font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-400 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:ring-offset-black"
15+
return className ? `${baseClasses} ${className}` : baseClasses
16+
}, [className])
1317

1418
if (href) {
1519
return (

src/components/SignIn.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const SignIn: React.FC<SignInProps> = ({
102102
((authProvider as any)?.formLayout || []).map((input: InputInfo) =>
103103
Object.assign({}, input, {
104104
type: input.type?.toLowerCase(),
105-
autocomplete: input.autocomplete || (input.type?.toLowerCase() === 'password' ? 'current-password' : undefined)
105+
autoComplete: input.autocomplete || (input.type?.toLowerCase() === 'password' ? 'current-password' : undefined)
106106
|| (input.id.toLowerCase() === 'username' ? 'username' : undefined),
107107
css: Object.assign({ field: 'col-span-12' }, input.css)
108108
})),

src/components/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ export interface AutocompleteProps {
226226
match:(item:any,value:string) => boolean
227227
viewCount?: number
228228
pageSize?: number
229+
className?: string
229230
onChange?: (value:any[]|any) => void
230231
children?: ((item:any) => ReactNode) | ReactNode
231232
}
@@ -238,6 +239,10 @@ export interface ComboboxProps {
238239
options?: any
239240
values?: string[]
240241
entries?: { key:string, value:string }[],
242+
label?: string
243+
help?: string
244+
placeholder?: string
245+
className?: string
241246
onChange?: (value:any[]|any) => void
242247
children?: ((item:any) => ReactNode) | ReactNode
243248
}

0 commit comments

Comments
 (0)