@opensaas/stack-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +8 -0
- package/README.md +286 -0
- package/dist/components/AdminUI.d.ts +24 -0
- package/dist/components/AdminUI.d.ts.map +1 -0
- package/dist/components/AdminUI.js +48 -0
- package/dist/components/ConfirmDialog.d.ts +16 -0
- package/dist/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/ConfirmDialog.js +11 -0
- package/dist/components/Dashboard.d.ts +12 -0
- package/dist/components/Dashboard.d.ts.map +1 -0
- package/dist/components/Dashboard.js +30 -0
- package/dist/components/ItemForm.d.ts +17 -0
- package/dist/components/ItemForm.d.ts.map +1 -0
- package/dist/components/ItemForm.js +97 -0
- package/dist/components/ItemFormClient.d.ts +22 -0
- package/dist/components/ItemFormClient.d.ts.map +1 -0
- package/dist/components/ItemFormClient.js +127 -0
- package/dist/components/ListView.d.ts +17 -0
- package/dist/components/ListView.d.ts.map +1 -0
- package/dist/components/ListView.js +76 -0
- package/dist/components/ListViewClient.d.ts +19 -0
- package/dist/components/ListViewClient.d.ts.map +1 -0
- package/dist/components/ListViewClient.js +108 -0
- package/dist/components/LoadingSpinner.d.ts +10 -0
- package/dist/components/LoadingSpinner.d.ts.map +1 -0
- package/dist/components/LoadingSpinner.js +14 -0
- package/dist/components/Navigation.d.ts +13 -0
- package/dist/components/Navigation.d.ts.map +1 -0
- package/dist/components/Navigation.js +20 -0
- package/dist/components/SkeletonLoader.d.ts +22 -0
- package/dist/components/SkeletonLoader.d.ts.map +1 -0
- package/dist/components/SkeletonLoader.js +25 -0
- package/dist/components/fields/CheckboxField.d.ts +11 -0
- package/dist/components/fields/CheckboxField.d.ts.map +1 -0
- package/dist/components/fields/CheckboxField.js +10 -0
- package/dist/components/fields/ComboboxField.d.ts +18 -0
- package/dist/components/fields/ComboboxField.d.ts.map +1 -0
- package/dist/components/fields/ComboboxField.js +32 -0
- package/dist/components/fields/FieldRenderer.d.ts +22 -0
- package/dist/components/fields/FieldRenderer.d.ts.map +1 -0
- package/dist/components/fields/FieldRenderer.js +81 -0
- package/dist/components/fields/IntegerField.d.ts +15 -0
- package/dist/components/fields/IntegerField.d.ts.map +1 -0
- package/dist/components/fields/IntegerField.js +14 -0
- package/dist/components/fields/PasswordField.d.ts +18 -0
- package/dist/components/fields/PasswordField.d.ts.map +1 -0
- package/dist/components/fields/PasswordField.js +42 -0
- package/dist/components/fields/RelationshipField.d.ts +20 -0
- package/dist/components/fields/RelationshipField.d.ts.map +1 -0
- package/dist/components/fields/RelationshipField.js +11 -0
- package/dist/components/fields/RelationshipManager.d.ts +19 -0
- package/dist/components/fields/RelationshipManager.d.ts.map +1 -0
- package/dist/components/fields/RelationshipManager.js +37 -0
- package/dist/components/fields/SelectField.d.ts +16 -0
- package/dist/components/fields/SelectField.d.ts.map +1 -0
- package/dist/components/fields/SelectField.js +11 -0
- package/dist/components/fields/TextField.d.ts +13 -0
- package/dist/components/fields/TextField.d.ts.map +1 -0
- package/dist/components/fields/TextField.js +11 -0
- package/dist/components/fields/TimestampField.d.ts +12 -0
- package/dist/components/fields/TimestampField.d.ts.map +1 -0
- package/dist/components/fields/TimestampField.js +12 -0
- package/dist/components/fields/index.d.ts +23 -0
- package/dist/components/fields/index.d.ts.map +1 -0
- package/dist/components/fields/index.js +13 -0
- package/dist/components/fields/registry.d.ts +43 -0
- package/dist/components/fields/registry.d.ts.map +1 -0
- package/dist/components/fields/registry.js +42 -0
- package/dist/components/standalone/DeleteButton.d.ts +35 -0
- package/dist/components/standalone/DeleteButton.d.ts.map +1 -0
- package/dist/components/standalone/DeleteButton.js +46 -0
- package/dist/components/standalone/ItemCreateForm.d.ts +34 -0
- package/dist/components/standalone/ItemCreateForm.d.ts.map +1 -0
- package/dist/components/standalone/ItemCreateForm.js +91 -0
- package/dist/components/standalone/ItemEditForm.d.ts +37 -0
- package/dist/components/standalone/ItemEditForm.d.ts.map +1 -0
- package/dist/components/standalone/ItemEditForm.js +112 -0
- package/dist/components/standalone/ListTable.d.ts +33 -0
- package/dist/components/standalone/ListTable.d.ts.map +1 -0
- package/dist/components/standalone/ListTable.js +94 -0
- package/dist/components/standalone/SearchBar.d.ts +29 -0
- package/dist/components/standalone/SearchBar.d.ts.map +1 -0
- package/dist/components/standalone/SearchBar.js +43 -0
- package/dist/components/standalone/index.d.ts +11 -0
- package/dist/components/standalone/index.d.ts.map +1 -0
- package/dist/components/standalone/index.js +6 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/lib/serializeFieldConfig.d.ts +43 -0
- package/dist/lib/serializeFieldConfig.d.ts.map +1 -0
- package/dist/lib/serializeFieldConfig.js +48 -0
- package/dist/lib/theme.d.ts +17 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +192 -0
- package/dist/lib/utils.d.ts +18 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +76 -0
- package/dist/primitives/button.d.ts +12 -0
- package/dist/primitives/button.d.ts.map +1 -0
- package/dist/primitives/button.js +33 -0
- package/dist/primitives/calendar.d.ts +9 -0
- package/dist/primitives/calendar.d.ts.map +1 -0
- package/dist/primitives/calendar.js +48 -0
- package/dist/primitives/card.d.ts +9 -0
- package/dist/primitives/card.d.ts.map +1 -0
- package/dist/primitives/card.js +16 -0
- package/dist/primitives/checkbox.d.ts +5 -0
- package/dist/primitives/checkbox.d.ts.map +1 -0
- package/dist/primitives/checkbox.js +7 -0
- package/dist/primitives/combobox.d.ts +14 -0
- package/dist/primitives/combobox.d.ts.map +1 -0
- package/dist/primitives/combobox.js +20 -0
- package/dist/primitives/datetime-picker.d.ts +9 -0
- package/dist/primitives/datetime-picker.d.ts.map +1 -0
- package/dist/primitives/datetime-picker.js +42 -0
- package/dist/primitives/dialog.d.ts +20 -0
- package/dist/primitives/dialog.d.ts.map +1 -0
- package/dist/primitives/dialog.js +21 -0
- package/dist/primitives/index.d.ts +14 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +14 -0
- package/dist/primitives/input.d.ts +5 -0
- package/dist/primitives/input.d.ts.map +1 -0
- package/dist/primitives/input.js +8 -0
- package/dist/primitives/label.d.ts +6 -0
- package/dist/primitives/label.d.ts.map +1 -0
- package/dist/primitives/label.js +9 -0
- package/dist/primitives/popover.d.ts +7 -0
- package/dist/primitives/popover.d.ts.map +1 -0
- package/dist/primitives/popover.js +10 -0
- package/dist/primitives/select.d.ts +14 -0
- package/dist/primitives/select.d.ts.map +1 -0
- package/dist/primitives/select.js +24 -0
- package/dist/primitives/table.d.ts +11 -0
- package/dist/primitives/table.d.ts.map +1 -0
- package/dist/primitives/table.js +20 -0
- package/dist/primitives/time-picker.d.ts +8 -0
- package/dist/primitives/time-picker.d.ts.map +1 -0
- package/dist/primitives/time-picker.js +27 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/types.d.ts +15 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +1 -0
- package/dist/styles/globals.css +1896 -0
- package/package.json +91 -0
- package/postcss.config.cjs +5 -0
- package/src/components/AdminUI.tsx +112 -0
- package/src/components/ConfirmDialog.tsx +56 -0
- package/src/components/Dashboard.tsx +134 -0
- package/src/components/ItemForm.tsx +195 -0
- package/src/components/ItemFormClient.tsx +237 -0
- package/src/components/ListView.tsx +153 -0
- package/src/components/ListViewClient.tsx +282 -0
- package/src/components/LoadingSpinner.tsx +32 -0
- package/src/components/Navigation.tsx +117 -0
- package/src/components/SkeletonLoader.tsx +82 -0
- package/src/components/fields/CheckboxField.tsx +54 -0
- package/src/components/fields/ComboboxField.tsx +127 -0
- package/src/components/fields/FieldRenderer.tsx +132 -0
- package/src/components/fields/IntegerField.tsx +68 -0
- package/src/components/fields/PasswordField.tsx +159 -0
- package/src/components/fields/RelationshipField.tsx +71 -0
- package/src/components/fields/RelationshipManager.tsx +189 -0
- package/src/components/fields/SelectField.tsx +71 -0
- package/src/components/fields/TextField.tsx +59 -0
- package/src/components/fields/TimestampField.tsx +49 -0
- package/src/components/fields/index.ts +27 -0
- package/src/components/fields/registry.ts +72 -0
- package/src/components/standalone/DeleteButton.tsx +114 -0
- package/src/components/standalone/ItemCreateForm.tsx +161 -0
- package/src/components/standalone/ItemEditForm.tsx +193 -0
- package/src/components/standalone/ListTable.tsx +211 -0
- package/src/components/standalone/SearchBar.tsx +86 -0
- package/src/components/standalone/index.ts +13 -0
- package/src/index.ts +74 -0
- package/src/lib/serializeFieldConfig.ts +88 -0
- package/src/lib/theme.ts +202 -0
- package/src/lib/utils.ts +81 -0
- package/src/primitives/button.tsx +49 -0
- package/src/primitives/calendar.tsx +160 -0
- package/src/primitives/card.tsx +58 -0
- package/src/primitives/checkbox.tsx +27 -0
- package/src/primitives/combobox.tsx +159 -0
- package/src/primitives/datetime-picker.tsx +130 -0
- package/src/primitives/dialog.tsx +108 -0
- package/src/primitives/index.ts +54 -0
- package/src/primitives/input.tsx +24 -0
- package/src/primitives/label.tsx +19 -0
- package/src/primitives/popover.tsx +31 -0
- package/src/primitives/select.tsx +158 -0
- package/src/primitives/table.tsx +91 -0
- package/src/primitives/time-picker.tsx +65 -0
- package/src/server/index.ts +3 -0
- package/src/server/types.ts +15 -0
- package/src/styles/globals.css +123 -0
- package/tailwind.config.ts +3 -0
- package/tests/components/TextField.test.tsx +94 -0
- package/tests/setup.ts +11 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Input } from '../../primitives/input.js'
|
|
4
|
+
import { Label } from '../../primitives/label.js'
|
|
5
|
+
import { Button } from '../../primitives/button.js'
|
|
6
|
+
import { cn } from '../../lib/utils.js'
|
|
7
|
+
import { useState } from 'react'
|
|
8
|
+
|
|
9
|
+
export interface PasswordFieldProps {
|
|
10
|
+
name: string
|
|
11
|
+
value: string | { isSet: boolean }
|
|
12
|
+
onChange: (value: string | { isSet: boolean } | undefined) => void
|
|
13
|
+
label: string
|
|
14
|
+
placeholder?: string
|
|
15
|
+
error?: string
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
required?: boolean
|
|
18
|
+
mode?: 'read' | 'edit'
|
|
19
|
+
showConfirm?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function PasswordField({
|
|
23
|
+
name,
|
|
24
|
+
value,
|
|
25
|
+
onChange,
|
|
26
|
+
label,
|
|
27
|
+
placeholder,
|
|
28
|
+
error,
|
|
29
|
+
disabled,
|
|
30
|
+
required,
|
|
31
|
+
mode = 'edit',
|
|
32
|
+
showConfirm = true,
|
|
33
|
+
}: PasswordFieldProps) {
|
|
34
|
+
// Check if value is the isSet object
|
|
35
|
+
const isSetObject = typeof value === 'object' && value !== null && 'isSet' in value
|
|
36
|
+
const isPasswordSet = isSetObject ? value.isSet : false
|
|
37
|
+
|
|
38
|
+
const [isChangingPassword, setIsChangingPassword] = useState(false)
|
|
39
|
+
const [passwordValue, setPasswordValue] = useState('')
|
|
40
|
+
const [confirmValue, setConfirmValue] = useState('')
|
|
41
|
+
const [showPassword, setShowPassword] = useState(false)
|
|
42
|
+
|
|
43
|
+
if (mode === 'read') {
|
|
44
|
+
return (
|
|
45
|
+
<div className="space-y-1">
|
|
46
|
+
<Label className="text-muted-foreground">{label}</Label>
|
|
47
|
+
<p className="text-sm">{isPasswordSet ? '••••••••' : 'Not set'}</p>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If not changing password and it's set, show the button
|
|
53
|
+
if (!isChangingPassword && isSetObject) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="space-y-2">
|
|
56
|
+
<Label>{label}</Label>
|
|
57
|
+
<div>
|
|
58
|
+
<Button
|
|
59
|
+
type="button"
|
|
60
|
+
variant="outline"
|
|
61
|
+
onClick={() => setIsChangingPassword(true)}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
>
|
|
64
|
+
{isPasswordSet ? 'Change Password' : 'Set Password'}
|
|
65
|
+
</Button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const confirmError =
|
|
72
|
+
showConfirm && passwordValue !== confirmValue && confirmValue !== ''
|
|
73
|
+
? 'Passwords do not match'
|
|
74
|
+
: undefined
|
|
75
|
+
|
|
76
|
+
const handleCancel = () => {
|
|
77
|
+
setIsChangingPassword(false)
|
|
78
|
+
setPasswordValue('')
|
|
79
|
+
setConfirmValue('')
|
|
80
|
+
setShowPassword(false)
|
|
81
|
+
// Reset to the isSet object
|
|
82
|
+
if (isSetObject) {
|
|
83
|
+
onChange(value)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handlePasswordChange = (newValue: string) => {
|
|
88
|
+
setPasswordValue(newValue)
|
|
89
|
+
// Update the parent with the actual password string
|
|
90
|
+
onChange(newValue || undefined)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="space-y-4">
|
|
95
|
+
<div className="space-y-2">
|
|
96
|
+
<Label htmlFor={name}>
|
|
97
|
+
{label}
|
|
98
|
+
{required && !isPasswordSet && <span className="text-destructive ml-1">*</span>}
|
|
99
|
+
</Label>
|
|
100
|
+
<div className="relative">
|
|
101
|
+
<Input
|
|
102
|
+
id={name}
|
|
103
|
+
name={name}
|
|
104
|
+
type={showPassword ? 'text' : 'password'}
|
|
105
|
+
value={passwordValue}
|
|
106
|
+
onChange={(e) => handlePasswordChange(e.target.value)}
|
|
107
|
+
placeholder={placeholder}
|
|
108
|
+
disabled={disabled}
|
|
109
|
+
required={required && !isPasswordSet}
|
|
110
|
+
className={cn('pr-10', error && 'border-destructive')}
|
|
111
|
+
/>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
115
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
116
|
+
>
|
|
117
|
+
{showPassword ? '👁️' : '👁️🗨️'}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{showConfirm && (
|
|
124
|
+
<div className="space-y-2">
|
|
125
|
+
<Label htmlFor={`${name}-confirm`}>
|
|
126
|
+
Confirm {label}
|
|
127
|
+
{required && !isPasswordSet && <span className="text-destructive ml-1">*</span>}
|
|
128
|
+
</Label>
|
|
129
|
+
<Input
|
|
130
|
+
id={`${name}-confirm`}
|
|
131
|
+
name={`${name}-confirm`}
|
|
132
|
+
type={showPassword ? 'text' : 'password'}
|
|
133
|
+
value={confirmValue}
|
|
134
|
+
onChange={(e) => setConfirmValue(e.target.value)}
|
|
135
|
+
placeholder={`Confirm ${placeholder || label.toLowerCase()}`}
|
|
136
|
+
disabled={disabled}
|
|
137
|
+
required={required && !isPasswordSet}
|
|
138
|
+
className={cn(confirmError && 'border-destructive')}
|
|
139
|
+
/>
|
|
140
|
+
{confirmError && <p className="text-sm text-destructive">{confirmError}</p>}
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{isSetObject && (
|
|
145
|
+
<div>
|
|
146
|
+
<Button
|
|
147
|
+
type="button"
|
|
148
|
+
variant="ghost"
|
|
149
|
+
size="sm"
|
|
150
|
+
onClick={handleCancel}
|
|
151
|
+
disabled={disabled}
|
|
152
|
+
>
|
|
153
|
+
Cancel
|
|
154
|
+
</Button>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ComboboxField } from './ComboboxField.js'
|
|
4
|
+
import { RelationshipManager } from './RelationshipManager.js'
|
|
5
|
+
|
|
6
|
+
export interface RelationshipFieldProps {
|
|
7
|
+
name: string
|
|
8
|
+
value: string | string[] | null
|
|
9
|
+
onChange: (value: string | string[] | null) => void
|
|
10
|
+
label: string
|
|
11
|
+
items: Array<{ id: string; label: string }>
|
|
12
|
+
error?: string
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
required?: boolean
|
|
15
|
+
mode?: 'read' | 'edit'
|
|
16
|
+
isLoading?: boolean
|
|
17
|
+
many?: boolean
|
|
18
|
+
relatedListKey?: string
|
|
19
|
+
basePath?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function RelationshipField({
|
|
23
|
+
name,
|
|
24
|
+
value,
|
|
25
|
+
onChange,
|
|
26
|
+
label,
|
|
27
|
+
items,
|
|
28
|
+
error,
|
|
29
|
+
disabled,
|
|
30
|
+
required,
|
|
31
|
+
mode = 'edit',
|
|
32
|
+
isLoading = false,
|
|
33
|
+
many = false,
|
|
34
|
+
relatedListKey,
|
|
35
|
+
basePath,
|
|
36
|
+
}: RelationshipFieldProps) {
|
|
37
|
+
// Delegate to specialized components based on cardinality
|
|
38
|
+
if (many) {
|
|
39
|
+
return (
|
|
40
|
+
<RelationshipManager
|
|
41
|
+
name={name}
|
|
42
|
+
value={Array.isArray(value) ? value : []}
|
|
43
|
+
onChange={onChange}
|
|
44
|
+
label={label}
|
|
45
|
+
items={items}
|
|
46
|
+
error={error}
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
required={required}
|
|
49
|
+
mode={mode}
|
|
50
|
+
isLoading={isLoading}
|
|
51
|
+
relatedListKey={relatedListKey}
|
|
52
|
+
basePath={basePath}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<ComboboxField
|
|
59
|
+
name={name}
|
|
60
|
+
value={typeof value === 'string' ? value : null}
|
|
61
|
+
onChange={onChange}
|
|
62
|
+
label={label}
|
|
63
|
+
items={items}
|
|
64
|
+
error={error}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
required={required}
|
|
67
|
+
mode={mode}
|
|
68
|
+
isLoading={isLoading}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from '../../primitives/table.js'
|
|
14
|
+
import {
|
|
15
|
+
Combobox,
|
|
16
|
+
ComboboxTrigger,
|
|
17
|
+
ComboboxContent,
|
|
18
|
+
ComboboxSearch,
|
|
19
|
+
ComboboxList,
|
|
20
|
+
ComboboxEmpty,
|
|
21
|
+
ComboboxItem,
|
|
22
|
+
} from '../../primitives/combobox.js'
|
|
23
|
+
import { Button } from '../../primitives/button.js'
|
|
24
|
+
|
|
25
|
+
export interface RelationshipManagerProps {
|
|
26
|
+
name: string
|
|
27
|
+
value: string[]
|
|
28
|
+
onChange: (value: string[]) => void
|
|
29
|
+
label: string
|
|
30
|
+
items: Array<{ id: string; label: string }>
|
|
31
|
+
error?: string
|
|
32
|
+
disabled?: boolean
|
|
33
|
+
required?: boolean
|
|
34
|
+
mode?: 'read' | 'edit'
|
|
35
|
+
isLoading?: boolean
|
|
36
|
+
relatedListKey?: string
|
|
37
|
+
basePath?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function RelationshipManager({
|
|
41
|
+
name: _name,
|
|
42
|
+
value,
|
|
43
|
+
onChange,
|
|
44
|
+
label,
|
|
45
|
+
items,
|
|
46
|
+
error,
|
|
47
|
+
disabled,
|
|
48
|
+
required,
|
|
49
|
+
mode = 'edit',
|
|
50
|
+
isLoading = false,
|
|
51
|
+
relatedListKey,
|
|
52
|
+
basePath = '/admin',
|
|
53
|
+
}: RelationshipManagerProps) {
|
|
54
|
+
const [showConnectModal, setShowConnectModal] = useState(false)
|
|
55
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
56
|
+
|
|
57
|
+
const selectedIds = Array.isArray(value) ? value : []
|
|
58
|
+
const selectedItems = items.filter((item) => selectedIds.includes(item.id))
|
|
59
|
+
const availableItems = items.filter((item) => !selectedIds.includes(item.id))
|
|
60
|
+
|
|
61
|
+
// Read mode
|
|
62
|
+
if (mode === 'read') {
|
|
63
|
+
return (
|
|
64
|
+
<div className="space-y-1">
|
|
65
|
+
<label className="text-sm font-medium text-muted-foreground">{label}</label>
|
|
66
|
+
<p className="text-sm">
|
|
67
|
+
{selectedItems.length > 0 ? selectedItems.map((item) => item.label).join(', ') : '-'}
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Filter available items based on search
|
|
74
|
+
const filteredAvailableItems = searchQuery
|
|
75
|
+
? availableItems.filter((item) => item.label.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
76
|
+
: availableItems
|
|
77
|
+
|
|
78
|
+
const handleRemove = (itemId: string) => {
|
|
79
|
+
onChange(selectedIds.filter((id) => id !== itemId))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const handleConnect = (itemId: string) => {
|
|
83
|
+
onChange([...selectedIds, itemId])
|
|
84
|
+
setShowConnectModal(false)
|
|
85
|
+
setSearchQuery('')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="space-y-2">
|
|
90
|
+
<label className="text-sm font-medium">
|
|
91
|
+
{label}
|
|
92
|
+
{required && <span className="text-destructive ml-1">*</span>}
|
|
93
|
+
</label>
|
|
94
|
+
|
|
95
|
+
{/* Selected Items Table */}
|
|
96
|
+
{selectedItems.length > 0 ? (
|
|
97
|
+
<div className="rounded-md border border-input">
|
|
98
|
+
<Table>
|
|
99
|
+
<TableHeader>
|
|
100
|
+
<TableRow>
|
|
101
|
+
<TableHead>Name</TableHead>
|
|
102
|
+
<TableHead className="w-[100px] text-right">Actions</TableHead>
|
|
103
|
+
</TableRow>
|
|
104
|
+
</TableHeader>
|
|
105
|
+
<TableBody>
|
|
106
|
+
{selectedItems.map((item) => (
|
|
107
|
+
<TableRow key={item.id}>
|
|
108
|
+
<TableCell>
|
|
109
|
+
{relatedListKey ? (
|
|
110
|
+
<Link
|
|
111
|
+
href={`${basePath}/${relatedListKey}/${item.id}`}
|
|
112
|
+
className="text-primary hover:underline"
|
|
113
|
+
>
|
|
114
|
+
{item.label}
|
|
115
|
+
</Link>
|
|
116
|
+
) : (
|
|
117
|
+
item.label
|
|
118
|
+
)}
|
|
119
|
+
</TableCell>
|
|
120
|
+
<TableCell className="text-right">
|
|
121
|
+
<Button
|
|
122
|
+
type="button"
|
|
123
|
+
variant="ghost"
|
|
124
|
+
size="sm"
|
|
125
|
+
onClick={() => handleRemove(item.id)}
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
>
|
|
128
|
+
Remove
|
|
129
|
+
</Button>
|
|
130
|
+
</TableCell>
|
|
131
|
+
</TableRow>
|
|
132
|
+
))}
|
|
133
|
+
</TableBody>
|
|
134
|
+
</Table>
|
|
135
|
+
</div>
|
|
136
|
+
) : (
|
|
137
|
+
<div className="rounded-md border border-input border-dashed p-8 text-center">
|
|
138
|
+
<p className="text-sm text-muted-foreground">
|
|
139
|
+
No items connected. Click "Connect Existing" to add items.
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{/* Action Buttons */}
|
|
145
|
+
<div className="flex gap-2">
|
|
146
|
+
<Combobox open={showConnectModal} onOpenChange={setShowConnectModal}>
|
|
147
|
+
<ComboboxTrigger
|
|
148
|
+
disabled={disabled || isLoading || availableItems.length === 0}
|
|
149
|
+
className="h-9 px-3"
|
|
150
|
+
>
|
|
151
|
+
<span>{isLoading ? 'Loading...' : 'Connect Existing'}</span>
|
|
152
|
+
</ComboboxTrigger>
|
|
153
|
+
<ComboboxContent>
|
|
154
|
+
<ComboboxSearch
|
|
155
|
+
placeholder="Search..."
|
|
156
|
+
value={searchQuery}
|
|
157
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
158
|
+
onKeyDown={(e) => {
|
|
159
|
+
if (e.key === 'Enter') {
|
|
160
|
+
e.preventDefault()
|
|
161
|
+
}
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
<ComboboxList>
|
|
165
|
+
{filteredAvailableItems.length === 0 ? (
|
|
166
|
+
<ComboboxEmpty>
|
|
167
|
+
{availableItems.length === 0
|
|
168
|
+
? 'All items are already connected'
|
|
169
|
+
: 'No results found'}
|
|
170
|
+
</ComboboxEmpty>
|
|
171
|
+
) : (
|
|
172
|
+
filteredAvailableItems.map((item) => (
|
|
173
|
+
<ComboboxItem key={item.id} onClick={() => handleConnect(item.id)}>
|
|
174
|
+
{item.label}
|
|
175
|
+
</ComboboxItem>
|
|
176
|
+
))
|
|
177
|
+
)}
|
|
178
|
+
</ComboboxList>
|
|
179
|
+
</ComboboxContent>
|
|
180
|
+
</Combobox>
|
|
181
|
+
|
|
182
|
+
{/* Note: "Create New" functionality would require additional props for the related list's fields
|
|
183
|
+
and form rendering logic. For now, we'll leave it as a placeholder or implement in a future iteration */}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{error && <p className="text-sm text-destructive mt-2">{error}</p>}
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from '../../primitives/select.js'
|
|
10
|
+
import { Label } from '../../primitives/label.js'
|
|
11
|
+
|
|
12
|
+
export interface SelectFieldProps {
|
|
13
|
+
name: string
|
|
14
|
+
value: string | null
|
|
15
|
+
onChange: (value: string | null) => void
|
|
16
|
+
label: string
|
|
17
|
+
options: Array<{ label: string; value: string }>
|
|
18
|
+
error?: string
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
required?: boolean
|
|
21
|
+
mode?: 'read' | 'edit'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function SelectField({
|
|
25
|
+
name,
|
|
26
|
+
value,
|
|
27
|
+
onChange,
|
|
28
|
+
label,
|
|
29
|
+
options,
|
|
30
|
+
error,
|
|
31
|
+
disabled,
|
|
32
|
+
required,
|
|
33
|
+
mode = 'edit',
|
|
34
|
+
}: SelectFieldProps) {
|
|
35
|
+
if (mode === 'read') {
|
|
36
|
+
const selectedOption = options.find((opt) => opt.value === value)
|
|
37
|
+
return (
|
|
38
|
+
<div className="space-y-1">
|
|
39
|
+
<Label className="text-muted-foreground">{label}</Label>
|
|
40
|
+
<p className="text-sm">{selectedOption?.label || '-'}</p>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="space-y-2">
|
|
47
|
+
<Label htmlFor={name}>
|
|
48
|
+
{label}
|
|
49
|
+
{required && <span className="text-destructive ml-1">*</span>}
|
|
50
|
+
</Label>
|
|
51
|
+
<Select
|
|
52
|
+
value={value || undefined}
|
|
53
|
+
onValueChange={(val) => onChange(val || null)}
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
required={required}
|
|
56
|
+
>
|
|
57
|
+
<SelectTrigger id={name} className={error ? 'border-destructive' : ''}>
|
|
58
|
+
<SelectValue placeholder="Select an option..." />
|
|
59
|
+
</SelectTrigger>
|
|
60
|
+
<SelectContent>
|
|
61
|
+
{options.map((option) => (
|
|
62
|
+
<SelectItem key={option.value} value={option.value}>
|
|
63
|
+
{option.label}
|
|
64
|
+
</SelectItem>
|
|
65
|
+
))}
|
|
66
|
+
</SelectContent>
|
|
67
|
+
</Select>
|
|
68
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Input } from '../../primitives/input.js'
|
|
4
|
+
import { Label } from '../../primitives/label.js'
|
|
5
|
+
import { cn } from '../../lib/utils.js'
|
|
6
|
+
|
|
7
|
+
export interface TextFieldProps {
|
|
8
|
+
name: string
|
|
9
|
+
value: string
|
|
10
|
+
onChange: (value: string) => void
|
|
11
|
+
label: string
|
|
12
|
+
placeholder?: string
|
|
13
|
+
error?: string
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
required?: boolean
|
|
16
|
+
mode?: 'read' | 'edit'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function TextField({
|
|
20
|
+
name,
|
|
21
|
+
value,
|
|
22
|
+
onChange,
|
|
23
|
+
label,
|
|
24
|
+
placeholder,
|
|
25
|
+
error,
|
|
26
|
+
disabled,
|
|
27
|
+
required,
|
|
28
|
+
mode = 'edit',
|
|
29
|
+
}: TextFieldProps) {
|
|
30
|
+
if (mode === 'read') {
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-1">
|
|
33
|
+
<Label className="text-muted-foreground">{label}</Label>
|
|
34
|
+
<p className="text-sm">{value || '-'}</p>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="space-y-2">
|
|
41
|
+
<Label htmlFor={name}>
|
|
42
|
+
{label}
|
|
43
|
+
{required && <span className="text-destructive ml-1">*</span>}
|
|
44
|
+
</Label>
|
|
45
|
+
<Input
|
|
46
|
+
id={name}
|
|
47
|
+
name={name}
|
|
48
|
+
type="text"
|
|
49
|
+
value={value || ''}
|
|
50
|
+
onChange={(e) => onChange(e.target.value)}
|
|
51
|
+
placeholder={placeholder}
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
required={required}
|
|
54
|
+
className={cn(error && 'border-destructive')}
|
|
55
|
+
/>
|
|
56
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Label } from '../../primitives/label.js'
|
|
4
|
+
import { DateTimePicker } from '../../primitives/datetime-picker.js'
|
|
5
|
+
import { format } from 'date-fns'
|
|
6
|
+
|
|
7
|
+
export interface TimestampFieldProps {
|
|
8
|
+
name: string
|
|
9
|
+
value: Date | string | null
|
|
10
|
+
onChange: (value: Date | null) => void
|
|
11
|
+
label: string
|
|
12
|
+
error?: string
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
required?: boolean
|
|
15
|
+
mode?: 'read' | 'edit'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function TimestampField({
|
|
19
|
+
name,
|
|
20
|
+
value,
|
|
21
|
+
onChange,
|
|
22
|
+
label,
|
|
23
|
+
error,
|
|
24
|
+
disabled,
|
|
25
|
+
required,
|
|
26
|
+
mode = 'edit',
|
|
27
|
+
}: TimestampFieldProps) {
|
|
28
|
+
const dateValue = value ? new Date(value) : null
|
|
29
|
+
|
|
30
|
+
if (mode === 'read') {
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-1">
|
|
33
|
+
<Label className="text-muted-foreground">{label}</Label>
|
|
34
|
+
<p className="text-sm">{dateValue ? format(dateValue, 'PPpp') : '-'}</p>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="space-y-2">
|
|
41
|
+
<Label htmlFor={name}>
|
|
42
|
+
{label}
|
|
43
|
+
{required && <span className="text-destructive ml-1">*</span>}
|
|
44
|
+
</Label>
|
|
45
|
+
<DateTimePicker value={dateValue} onChange={onChange} disabled={disabled} />
|
|
46
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Field components
|
|
2
|
+
export { TextField } from './TextField.js'
|
|
3
|
+
export { IntegerField } from './IntegerField.js'
|
|
4
|
+
export { CheckboxField } from './CheckboxField.js'
|
|
5
|
+
export { SelectField } from './SelectField.js'
|
|
6
|
+
export { TimestampField } from './TimestampField.js'
|
|
7
|
+
export { PasswordField } from './PasswordField.js'
|
|
8
|
+
export { RelationshipField } from './RelationshipField.js'
|
|
9
|
+
export { ComboboxField } from './ComboboxField.js'
|
|
10
|
+
export { RelationshipManager } from './RelationshipManager.js'
|
|
11
|
+
export { FieldRenderer } from './FieldRenderer.js'
|
|
12
|
+
|
|
13
|
+
// Registry for custom field types
|
|
14
|
+
export { fieldComponentRegistry, registerFieldComponent, getFieldComponent } from './registry.js'
|
|
15
|
+
|
|
16
|
+
// Re-export types
|
|
17
|
+
export type { TextFieldProps } from './TextField.js'
|
|
18
|
+
export type { IntegerFieldProps } from './IntegerField.js'
|
|
19
|
+
export type { CheckboxFieldProps } from './CheckboxField.js'
|
|
20
|
+
export type { SelectFieldProps } from './SelectField.js'
|
|
21
|
+
export type { TimestampFieldProps } from './TimestampField.js'
|
|
22
|
+
export type { PasswordFieldProps } from './PasswordField.js'
|
|
23
|
+
export type { RelationshipFieldProps } from './RelationshipField.js'
|
|
24
|
+
export type { ComboboxFieldProps } from './ComboboxField.js'
|
|
25
|
+
export type { RelationshipManagerProps } from './RelationshipManager.js'
|
|
26
|
+
export type { FieldRendererProps } from './FieldRenderer.js'
|
|
27
|
+
export type { FieldComponent, FieldComponentProps } from './registry.js'
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ComponentType } from 'react'
|
|
2
|
+
import { TextField } from './TextField.js'
|
|
3
|
+
import { IntegerField } from './IntegerField.js'
|
|
4
|
+
import { CheckboxField } from './CheckboxField.js'
|
|
5
|
+
import { SelectField } from './SelectField.js'
|
|
6
|
+
import { TimestampField } from './TimestampField.js'
|
|
7
|
+
import { PasswordField } from './PasswordField.js'
|
|
8
|
+
import { RelationshipField } from './RelationshipField.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Base props that all field components must accept
|
|
12
|
+
* Field components can extend this with additional field-specific props
|
|
13
|
+
*/
|
|
14
|
+
export type FieldComponentProps = {
|
|
15
|
+
name: string
|
|
16
|
+
value: unknown
|
|
17
|
+
onChange: (value: unknown) => void
|
|
18
|
+
label: string
|
|
19
|
+
error?: string
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
required?: boolean
|
|
22
|
+
mode?: 'read' | 'edit'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type for field component
|
|
27
|
+
* Field components must accept props that extend FieldComponentProps.
|
|
28
|
+
* The registry uses ComponentType<any> because components have different
|
|
29
|
+
* specific prop types (e.g., value: string vs value: number), but all
|
|
30
|
+
* must include the base FieldComponentProps structure.
|
|
31
|
+
*/
|
|
32
|
+
export type FieldComponent = ComponentType<FieldComponentProps & Record<string, unknown>>
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Registry mapping field types to their default UI components
|
|
36
|
+
* This can be extended for custom field types
|
|
37
|
+
* Uses ComponentType<any> to allow components with more specific prop types
|
|
38
|
+
*/
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
export const fieldComponentRegistry: Record<string, ComponentType<any>> = {
|
|
41
|
+
text: TextField,
|
|
42
|
+
integer: IntegerField,
|
|
43
|
+
checkbox: CheckboxField,
|
|
44
|
+
select: SelectField,
|
|
45
|
+
timestamp: TimestampField,
|
|
46
|
+
password: PasswordField,
|
|
47
|
+
relationship: RelationshipField,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register a custom field component for a field type
|
|
52
|
+
* Useful for adding support for custom field types
|
|
53
|
+
*
|
|
54
|
+
* @param fieldType - The field type identifier
|
|
55
|
+
* @param component - A React component that accepts FieldComponentProps (and optionally additional props)
|
|
56
|
+
*/
|
|
57
|
+
export function registerFieldComponent(
|
|
58
|
+
fieldType: string,
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
component: ComponentType<any>,
|
|
61
|
+
): void {
|
|
62
|
+
fieldComponentRegistry[fieldType] = component
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the component for a field type
|
|
67
|
+
* Returns undefined if no component is registered
|
|
68
|
+
*/
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
export function getFieldComponent(fieldType: string): ComponentType<any> | undefined {
|
|
71
|
+
return fieldComponentRegistry[fieldType]
|
|
72
|
+
}
|