@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,127 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useTransition } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { FieldRenderer } from './fields/FieldRenderer.js';
|
|
6
|
+
import { ConfirmDialog } from './ConfirmDialog.js';
|
|
7
|
+
import { LoadingSpinner } from './LoadingSpinner.js';
|
|
8
|
+
import { Button } from '../primitives/button.js';
|
|
9
|
+
/**
|
|
10
|
+
* Client component for interactive form
|
|
11
|
+
* Handles form state, validation, and submission
|
|
12
|
+
*/
|
|
13
|
+
export function ItemFormClient({ listKey, urlKey, mode, fields, initialData = {}, itemId, basePath, serverAction, relationshipData = {}, }) {
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const [isPending, startTransition] = useTransition();
|
|
16
|
+
const [formData, setFormData] = useState(initialData);
|
|
17
|
+
const [errors, setErrors] = useState({});
|
|
18
|
+
const [generalError, setGeneralError] = useState(null);
|
|
19
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
20
|
+
const handleFieldChange = (fieldName, value) => {
|
|
21
|
+
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
|
22
|
+
// Clear error for this field when user starts typing
|
|
23
|
+
if (errors[fieldName]) {
|
|
24
|
+
setErrors((prev) => {
|
|
25
|
+
const newErrors = { ...prev };
|
|
26
|
+
delete newErrors[fieldName];
|
|
27
|
+
return newErrors;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const handleSubmit = async (e) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
setErrors({});
|
|
34
|
+
setGeneralError(null);
|
|
35
|
+
startTransition(async () => {
|
|
36
|
+
try {
|
|
37
|
+
// Transform relationship fields to Prisma format
|
|
38
|
+
// Filter out password fields with isSet objects (unchanged passwords)
|
|
39
|
+
const transformedData = {};
|
|
40
|
+
for (const [fieldName, value] of Object.entries(formData)) {
|
|
41
|
+
const fieldConfig = fields[fieldName];
|
|
42
|
+
// Skip password fields that have { isSet: boolean } value (not being changed)
|
|
43
|
+
if (typeof value === 'object' && value !== null && 'isSet' in value) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Transform relationship fields - check discriminated union type
|
|
47
|
+
const fieldAny = fieldConfig;
|
|
48
|
+
if (fieldAny?.type === 'relationship') {
|
|
49
|
+
if (fieldAny.many) {
|
|
50
|
+
// Many relationship: use connect format
|
|
51
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
52
|
+
transformedData[fieldName] = {
|
|
53
|
+
connect: value.map((id) => ({ id })),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Single relationship: use connect format
|
|
59
|
+
if (value) {
|
|
60
|
+
transformedData[fieldName] = {
|
|
61
|
+
connect: { id: value },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Non-relationship field: pass through
|
|
68
|
+
transformedData[fieldName] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const result = mode === 'create'
|
|
72
|
+
? await serverAction({
|
|
73
|
+
listKey,
|
|
74
|
+
action: 'create',
|
|
75
|
+
data: transformedData,
|
|
76
|
+
})
|
|
77
|
+
: await serverAction({
|
|
78
|
+
listKey,
|
|
79
|
+
action: 'update',
|
|
80
|
+
id: itemId,
|
|
81
|
+
data: transformedData,
|
|
82
|
+
});
|
|
83
|
+
if (result) {
|
|
84
|
+
// Navigate back to list view
|
|
85
|
+
router.push(`${basePath}/${urlKey}`);
|
|
86
|
+
router.refresh();
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
setGeneralError('Access denied or operation failed');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to save item';
|
|
94
|
+
setGeneralError(errorMessage);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const handleDelete = async () => {
|
|
99
|
+
if (!itemId)
|
|
100
|
+
return;
|
|
101
|
+
setGeneralError(null);
|
|
102
|
+
setShowDeleteConfirm(false);
|
|
103
|
+
startTransition(async () => {
|
|
104
|
+
try {
|
|
105
|
+
const result = await serverAction({
|
|
106
|
+
listKey,
|
|
107
|
+
action: 'delete',
|
|
108
|
+
id: itemId,
|
|
109
|
+
});
|
|
110
|
+
if (result) {
|
|
111
|
+
router.push(`${basePath}/${urlKey}`);
|
|
112
|
+
router.refresh();
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
setGeneralError('Access denied or failed to delete item');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to delete item';
|
|
120
|
+
setGeneralError(errorMessage);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
// Filter out system fields
|
|
125
|
+
const editableFields = Object.entries(fields).filter(([key]) => !['id', 'createdAt', 'updatedAt'].includes(key));
|
|
126
|
+
return (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-6", children: [generalError && (_jsx("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-4", children: _jsx("p", { className: "text-sm font-medium", children: generalError }) })), _jsx("div", { className: "space-y-6", children: editableFields.map(([fieldName, fieldConfig]) => (_jsx(FieldRenderer, { fieldName: fieldName, fieldConfig: fieldConfig, value: formData[fieldName], onChange: (value) => handleFieldChange(fieldName, value), error: errors[fieldName], disabled: isPending, mode: "edit", relationshipItems: relationshipData[fieldName] || [], relationshipLoading: false, basePath: basePath }, fieldName))) }), _jsxs("div", { className: "flex items-center justify-between pt-6 border-t border-border", children: [_jsxs("div", { className: "flex gap-3", children: [_jsxs(Button, { type: "submit", disabled: isPending, className: "gap-2", children: [isPending && (_jsx(LoadingSpinner, { size: "sm", className: "border-primary-foreground border-t-transparent" })), isPending ? 'Saving...' : mode === 'create' ? 'Create' : 'Save'] }), _jsx(Button, { type: "button", variant: "secondary", onClick: () => router.push(`${basePath}/${urlKey}`), disabled: isPending, children: "Cancel" })] }), mode === 'edit' && itemId && (_jsx(Button, { type: "button", variant: "destructive", onClick: () => setShowDeleteConfirm(true), disabled: isPending, children: "Delete" }))] }), _jsx(ConfirmDialog, { isOpen: showDeleteConfirm, title: "Delete Item", message: "Are you sure you want to delete this item? This action cannot be undone.", confirmLabel: "Delete", cancelLabel: "Cancel", variant: "danger", onConfirm: handleDelete, onCancel: () => setShowDeleteConfirm(false) })] }));
|
|
127
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AccessContext, OpenSaasConfig, type PrismaClientLike } from '@opensaas/stack-core';
|
|
2
|
+
export interface ListViewProps<TPrisma extends PrismaClientLike = PrismaClientLike> {
|
|
3
|
+
context: AccessContext<TPrisma>;
|
|
4
|
+
config: OpenSaasConfig;
|
|
5
|
+
listKey: string;
|
|
6
|
+
basePath?: string;
|
|
7
|
+
columns?: string[];
|
|
8
|
+
page?: number;
|
|
9
|
+
pageSize?: number;
|
|
10
|
+
search?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* List view component - displays items in a table
|
|
14
|
+
* Server Component that fetches data and renders client table
|
|
15
|
+
*/
|
|
16
|
+
export declare function ListView<TPrisma extends PrismaClientLike = PrismaClientLike>({ context, config, listKey, basePath, columns, page, pageSize, search, }: ListViewProps<TPrisma>): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
17
|
+
//# sourceMappingURL=ListView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListView.d.ts","sourceRoot":"","sources":["../../src/components/ListView.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EAGb,cAAc,EACd,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAA;AAE7B,MAAM,WAAW,aAAa,CAAC,OAAO,SAAS,gBAAgB,GAAG,gBAAgB;IAChF,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,SAAS,gBAAgB,GAAG,gBAAgB,EAAE,EAClF,OAAO,EACP,MAAM,EACN,OAAO,EACP,QAAmB,EACnB,OAAO,EACP,IAAQ,EACR,QAAa,EACb,MAAM,GACP,EAAE,aAAa,CAAC,OAAO,CAAC,oDAqHxB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
import { ListViewClient } from './ListViewClient.js';
|
|
4
|
+
import { formatListName } from '../lib/utils.js';
|
|
5
|
+
import { getDbKey, getUrlKey, } from '@opensaas/stack-core';
|
|
6
|
+
/**
|
|
7
|
+
* List view component - displays items in a table
|
|
8
|
+
* Server Component that fetches data and renders client table
|
|
9
|
+
*/
|
|
10
|
+
export async function ListView({ context, config, listKey, basePath = '/admin', columns, page = 1, pageSize = 50, search, }) {
|
|
11
|
+
const key = getDbKey(listKey);
|
|
12
|
+
const urlKey = getUrlKey(listKey);
|
|
13
|
+
const listConfig = config.lists[listKey];
|
|
14
|
+
if (!listConfig) {
|
|
15
|
+
return (_jsx("div", { className: "p-8", children: _jsxs("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-2", children: "List not found" }), _jsxs("p", { children: ["The list \"", listKey, "\" does not exist in your configuration."] })] }) }));
|
|
16
|
+
}
|
|
17
|
+
// Fetch items using access-controlled context
|
|
18
|
+
const skip = (page - 1) * pageSize;
|
|
19
|
+
let items = [];
|
|
20
|
+
let total = 0;
|
|
21
|
+
try {
|
|
22
|
+
const dbContext = context.db;
|
|
23
|
+
if (!dbContext || !dbContext[key]) {
|
|
24
|
+
throw new Error(`Context for ${listKey} not found`);
|
|
25
|
+
}
|
|
26
|
+
// Build search filter if search term provided
|
|
27
|
+
let where = undefined;
|
|
28
|
+
if (search && search.trim()) {
|
|
29
|
+
// Find all text fields to search across
|
|
30
|
+
const searchableFields = Object.entries(listConfig.fields)
|
|
31
|
+
.filter(([_, field]) => field.type === 'text')
|
|
32
|
+
.map(([fieldName]) => fieldName);
|
|
33
|
+
if (searchableFields.length > 0) {
|
|
34
|
+
where = {
|
|
35
|
+
OR: searchableFields.map((fieldName) => ({
|
|
36
|
+
[fieldName]: {
|
|
37
|
+
contains: search.trim(),
|
|
38
|
+
},
|
|
39
|
+
})),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Build include object for relationship fields
|
|
44
|
+
const include = {};
|
|
45
|
+
Object.entries(listConfig.fields).forEach(([fieldName, field]) => {
|
|
46
|
+
if (field.type === 'relationship') {
|
|
47
|
+
include[fieldName] = true;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
[items, total] = await Promise.all([
|
|
51
|
+
dbContext[key].findMany({
|
|
52
|
+
where,
|
|
53
|
+
skip,
|
|
54
|
+
take: pageSize,
|
|
55
|
+
...(Object.keys(include).length > 0 ? { include } : {}),
|
|
56
|
+
}),
|
|
57
|
+
dbContext[key].count({ where }),
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error(`Failed to fetch ${listKey}:`, error);
|
|
62
|
+
}
|
|
63
|
+
// Serialize items for client component (convert Dates, etc to JSON-safe format)
|
|
64
|
+
const serializedItems = JSON.parse(JSON.stringify(items));
|
|
65
|
+
// Extract only the relationship refs needed by client (don't send entire config)
|
|
66
|
+
const relationshipRefs = {};
|
|
67
|
+
Object.entries(listConfig.fields).forEach(([fieldName, field]) => {
|
|
68
|
+
if ('type' in field && field.type === 'relationship' && 'ref' in field && field.ref) {
|
|
69
|
+
relationshipRefs[fieldName] = field.ref;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return (_jsxs("div", { className: "p-8", children: [_jsxs("div", { className: "flex items-center justify-between mb-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-2", children: formatListName(listKey) }), _jsxs("p", { className: "text-muted-foreground", children: [total, " ", total === 1 ? 'item' : 'items'] })] }), _jsxs(Link, { href: `${basePath}/${urlKey}/create`, className: "inline-flex items-center px-4 py-2 bg-primary text-primary-foreground rounded-md font-medium hover:bg-primary/90 transition-colors", children: [_jsx("span", { className: "mr-2", children: "+" }), "Create ", formatListName(listKey)] })] }), _jsx(ListViewClient, { items: serializedItems || [], fieldTypes: Object.fromEntries(Object.entries(listConfig.fields).map(([key, field]) => [
|
|
73
|
+
key,
|
|
74
|
+
field.type,
|
|
75
|
+
])), relationshipRefs: relationshipRefs, columns: columns, listKey: listKey, urlKey: urlKey, basePath: basePath, page: page, pageSize: pageSize, total: total || 0, search: search })] }));
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ListViewClientProps {
|
|
2
|
+
items: Array<Record<string, unknown>>;
|
|
3
|
+
fieldTypes: Record<string, string>;
|
|
4
|
+
relationshipRefs: Record<string, string>;
|
|
5
|
+
columns?: string[];
|
|
6
|
+
listKey: string;
|
|
7
|
+
urlKey: string;
|
|
8
|
+
basePath: string;
|
|
9
|
+
page: number;
|
|
10
|
+
pageSize: number;
|
|
11
|
+
total: number;
|
|
12
|
+
search?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Client component for interactive list table
|
|
16
|
+
* Handles sorting, pagination, and row interactions
|
|
17
|
+
*/
|
|
18
|
+
export declare function ListViewClient({ items, fieldTypes, relationshipRefs, columns, urlKey, basePath, page, pageSize, total, search: initialSearch, }: ListViewClientProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=ListViewClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListViewClient.d.ts","sourceRoot":"","sources":["../../src/components/ListViewClient.tsx"],"names":[],"mappings":"AAoBA,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACxC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,UAAU,EACV,gBAAgB,EAChB,OAAO,EACP,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,KAAK,EACL,MAAM,EAAE,aAAa,GACtB,EAAE,mBAAmB,2CAwOrB"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { useRouter } from 'next/navigation';
|
|
7
|
+
import { formatFieldName, getFieldDisplayValue } from '../lib/utils.js';
|
|
8
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '../primitives/table.js';
|
|
9
|
+
import { Input } from '../primitives/input.js';
|
|
10
|
+
import { Button } from '../primitives/button.js';
|
|
11
|
+
import { Card } from '../primitives/card.js';
|
|
12
|
+
import { getUrlKey } from '@opensaas/stack-core';
|
|
13
|
+
/**
|
|
14
|
+
* Client component for interactive list table
|
|
15
|
+
* Handles sorting, pagination, and row interactions
|
|
16
|
+
*/
|
|
17
|
+
export function ListViewClient({ items, fieldTypes, relationshipRefs, columns, urlKey, basePath, page, pageSize, total, search: initialSearch, }) {
|
|
18
|
+
const router = useRouter();
|
|
19
|
+
const [sortBy, setSortBy] = useState(null);
|
|
20
|
+
const [sortOrder, setSortOrder] = useState('asc');
|
|
21
|
+
const [searchInput, setSearchInput] = useState(initialSearch || '');
|
|
22
|
+
// Determine which columns to show
|
|
23
|
+
const displayColumns = columns ||
|
|
24
|
+
Object.keys(fieldTypes).filter((key) => !['password', 'createdAt', 'updatedAt'].includes(key));
|
|
25
|
+
// Sort items if needed
|
|
26
|
+
const sortedItems = [...items];
|
|
27
|
+
if (sortBy) {
|
|
28
|
+
sortedItems.sort((a, b) => {
|
|
29
|
+
const aVal = a[sortBy];
|
|
30
|
+
const bVal = b[sortBy];
|
|
31
|
+
if (aVal === bVal)
|
|
32
|
+
return 0;
|
|
33
|
+
// Handle unknown types for comparison - convert to string for safety
|
|
34
|
+
const aStr = String(aVal ?? '');
|
|
35
|
+
const bStr = String(bVal ?? '');
|
|
36
|
+
const comparison = aStr > bStr ? 1 : -1;
|
|
37
|
+
return sortOrder === 'asc' ? comparison : -comparison;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
41
|
+
const hasNextPage = page < totalPages;
|
|
42
|
+
const hasPrevPage = page > 1;
|
|
43
|
+
const handleSort = (column) => {
|
|
44
|
+
if (sortBy === column) {
|
|
45
|
+
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
setSortBy(column);
|
|
49
|
+
setSortOrder('asc');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const handleSearch = (e) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
const params = new URLSearchParams();
|
|
55
|
+
if (searchInput.trim()) {
|
|
56
|
+
params.set('search', searchInput.trim());
|
|
57
|
+
}
|
|
58
|
+
params.set('page', '1'); // Reset to page 1 on new search
|
|
59
|
+
router.push(`${basePath}/${urlKey}?${params.toString()}`);
|
|
60
|
+
};
|
|
61
|
+
const handleClearSearch = () => {
|
|
62
|
+
setSearchInput('');
|
|
63
|
+
router.push(`${basePath}/${urlKey}`);
|
|
64
|
+
};
|
|
65
|
+
const buildPaginationUrl = (newPage) => {
|
|
66
|
+
const params = new URLSearchParams();
|
|
67
|
+
if (initialSearch) {
|
|
68
|
+
params.set('search', initialSearch);
|
|
69
|
+
}
|
|
70
|
+
params.set('page', newPage.toString());
|
|
71
|
+
return `${basePath}/${urlKey}?${params.toString()}`;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Render a relationship field as a clickable link or links
|
|
75
|
+
*/
|
|
76
|
+
const renderRelationshipCell = (value, fieldName) => {
|
|
77
|
+
const ref = relationshipRefs[fieldName];
|
|
78
|
+
if (!ref) {
|
|
79
|
+
return getFieldDisplayValue(value, 'relationship');
|
|
80
|
+
}
|
|
81
|
+
// Parse ref to get related list name
|
|
82
|
+
const [relatedListKey] = ref.split('.');
|
|
83
|
+
const relatedUrlKey = getUrlKey(relatedListKey);
|
|
84
|
+
if (!value || typeof value !== 'object') {
|
|
85
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
86
|
+
}
|
|
87
|
+
// Handle array of relationships (many: true)
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
if (value.length === 0)
|
|
90
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
91
|
+
return (_jsx("span", { className: "flex flex-wrap gap-1", children: value.map((item, idx) => {
|
|
92
|
+
if (!item || typeof item !== 'object')
|
|
93
|
+
return null;
|
|
94
|
+
const displayValue = getFieldDisplayValue(item, 'relationship');
|
|
95
|
+
const itemId = 'id' in item ? item.id : null;
|
|
96
|
+
const key = itemId || idx;
|
|
97
|
+
return (_jsxs(React.Fragment, { children: [idx > 0 && _jsx("span", { className: "text-muted-foreground", children: ", " }), _jsx(Link, { href: `${basePath}/${relatedUrlKey}/${itemId}`, className: "text-primary hover:underline", onClick: (e) => e.stopPropagation(), children: displayValue })] }, key));
|
|
98
|
+
}) }));
|
|
99
|
+
}
|
|
100
|
+
// Handle single relationship
|
|
101
|
+
const itemId = 'id' in value ? value.id : null;
|
|
102
|
+
const displayValue = getFieldDisplayValue(value, 'relationship');
|
|
103
|
+
return (_jsx(Link, { href: `${basePath}/${relatedUrlKey}/${itemId}`, className: "text-primary hover:underline", onClick: (e) => e.stopPropagation(), children: displayValue }));
|
|
104
|
+
};
|
|
105
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(Card, { className: "p-4", children: _jsxs("form", { onSubmit: handleSearch, className: "flex gap-2", children: [_jsxs("div", { className: "flex-1 relative", children: [_jsx(Input, { type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), placeholder: "Search...", className: "pr-10" }), searchInput && (_jsx("button", { type: "button", onClick: handleClearSearch, className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground", children: "\u2715" }))] }), _jsx(Button, { type: "submit", children: "Search" })] }) }), _jsx("div", { className: "rounded-lg border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [displayColumns.map((column) => (_jsx(TableHead, { className: "cursor-pointer hover:bg-muted/70 transition-colors", onClick: () => handleSort(column), children: _jsxs("div", { className: "flex items-center space-x-1", children: [_jsx("span", { children: formatFieldName(column) }), sortBy === column && (_jsx("span", { className: "text-primary", children: sortOrder === 'asc' ? '↑' : '↓' }))] }) }, column))), _jsx(TableHead, { className: "text-right", children: "Actions" })] }) }), _jsx(TableBody, { children: sortedItems.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: displayColumns.length + 1, className: "h-24 text-center", children: "No items found" }) })) : (sortedItems.map((item) => (_jsxs(TableRow, { children: [displayColumns.map((column) => (_jsx(TableCell, { children: fieldTypes[column] === 'relationship'
|
|
106
|
+
? renderRelationshipCell(item[column], column)
|
|
107
|
+
: getFieldDisplayValue(item[column], fieldTypes[column]) }, column))), _jsx(TableCell, { className: "text-right", children: _jsx(Link, { href: `${basePath}/${urlKey}/${item.id}`, className: "text-primary hover:underline", children: "Edit" }) })] }, String(item.id))))) })] }) }), totalPages > 1 && (_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Showing ", (page - 1) * pageSize + 1, " to ", Math.min(page * pageSize, total), " of ", total, ' ', "results"] }), _jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(Button, { variant: "outline", onClick: () => router.push(buildPaginationUrl(page - 1)), disabled: !hasPrevPage, children: "Previous" }), _jsxs("span", { className: "text-sm text-muted-foreground", children: ["Page ", page, " of ", totalPages] }), _jsx(Button, { variant: "outline", onClick: () => router.push(buildPaginationUrl(page + 1)), disabled: !hasNextPage, children: "Next" })] })] }))] }));
|
|
108
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface LoadingSpinnerProps {
|
|
2
|
+
size?: 'sm' | 'md' | 'lg';
|
|
3
|
+
className?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Loading spinner component
|
|
7
|
+
* Used to indicate loading states
|
|
8
|
+
*/
|
|
9
|
+
export declare function LoadingSpinner({ size, className }: LoadingSpinnerProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=LoadingSpinner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LoadingSpinner.d.ts","sourceRoot":"","sources":["../../src/components/LoadingSpinner.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,IAAW,EAAE,SAAS,EAAE,EAAE,mBAAmB,2CAoB7E"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../lib/utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Loading spinner component
|
|
5
|
+
* Used to indicate loading states
|
|
6
|
+
*/
|
|
7
|
+
export function LoadingSpinner({ size = 'md', className }) {
|
|
8
|
+
const sizeClasses = {
|
|
9
|
+
sm: 'h-4 w-4 border-2',
|
|
10
|
+
md: 'h-8 w-8 border-2',
|
|
11
|
+
lg: 'h-12 w-12 border-3',
|
|
12
|
+
};
|
|
13
|
+
return (_jsx("div", { className: cn('animate-spin rounded-full border-primary border-t-transparent', sizeClasses[size], className), role: "status", "aria-label": "Loading", children: _jsx("span", { className: "sr-only", children: "Loading..." }) }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
2
|
+
export interface NavigationProps<TPrisma> {
|
|
3
|
+
context: AccessContext<TPrisma>;
|
|
4
|
+
config: OpenSaasConfig;
|
|
5
|
+
basePath?: string;
|
|
6
|
+
currentPath?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Navigation sidebar showing all lists
|
|
10
|
+
* Server Component
|
|
11
|
+
*/
|
|
12
|
+
export declare function Navigation<TPrisma>({ context, config, basePath, currentPath, }: NavigationProps<TPrisma>): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=Navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Navigation.d.ts","sourceRoot":"","sources":["../../src/components/Navigation.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAa,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE/E,MAAM,WAAW,eAAe,CAAC,OAAO;IACtC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,EAClC,OAAO,EACP,MAAM,EACN,QAAmB,EACnB,WAAgB,GACjB,EAAE,eAAe,CAAC,OAAO,CAAC,2CAgG1B"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
import { formatListName } from '../lib/utils.js';
|
|
4
|
+
import { getUrlKey } from '@opensaas/stack-core';
|
|
5
|
+
/**
|
|
6
|
+
* Navigation sidebar showing all lists
|
|
7
|
+
* Server Component
|
|
8
|
+
*/
|
|
9
|
+
export function Navigation({ context, config, basePath = '/admin', currentPath = '', }) {
|
|
10
|
+
const lists = Object.keys(config.lists || {});
|
|
11
|
+
return (_jsxs("nav", { className: "w-64 border-r border-border bg-card h-screen sticky top-0 flex flex-col", children: [_jsxs("div", { className: "p-6 border-b border-border relative overflow-hidden", children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-br from-primary/10 to-accent/10 opacity-50" }), _jsx(Link, { href: basePath, className: "block relative", children: _jsx("h1", { className: "text-xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent", children: "OpenSaas Admin" }) })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-4", children: _jsxs("div", { className: "space-y-1", children: [_jsxs(Link, { href: basePath, className: `block px-3 py-2.5 rounded-lg text-sm font-medium transition-all relative overflow-hidden group ${currentPath === ''
|
|
12
|
+
? 'bg-gradient-to-r from-primary to-accent text-primary-foreground shadow-lg shadow-primary/25'
|
|
13
|
+
: 'text-foreground hover:bg-accent/50 hover:text-accent-foreground'}`, children: [currentPath === '' && (_jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-primary/20 to-accent/20 animate-pulse" })), _jsxs("span", { className: "relative flex items-center gap-2", children: [_jsx("span", { className: currentPath === '' ? 'text-lg' : 'text-base', children: "\uD83D\uDCCA" }), "Dashboard"] })] }), lists.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { className: "pt-4 pb-2 px-3", children: _jsx("p", { className: "text-xs font-semibold text-muted-foreground uppercase tracking-wider", children: "Lists" }) }), lists.map((listKey) => {
|
|
14
|
+
const urlKey = getUrlKey(listKey);
|
|
15
|
+
const isActive = currentPath.startsWith(`/${urlKey}`);
|
|
16
|
+
return (_jsxs(Link, { href: `${basePath}/${urlKey}`, className: `block px-3 py-2.5 rounded-lg text-sm font-medium transition-all relative overflow-hidden group ${isActive
|
|
17
|
+
? 'bg-gradient-to-r from-primary to-accent text-primary-foreground shadow-lg shadow-primary/25'
|
|
18
|
+
: 'text-foreground hover:bg-accent/50 hover:text-accent-foreground'}`, children: [isActive && (_jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-primary/20 to-accent/20 animate-pulse" })), _jsxs("span", { className: "relative flex items-center gap-2", children: [_jsx("span", { className: "opacity-60 group-hover:opacity-100 transition-opacity", children: "\uD83D\uDCC1" }), formatListName(listKey)] })] }, listKey));
|
|
19
|
+
})] }))] }) }), context.session && (_jsx("div", { className: "p-4 border-t border-border bg-gradient-to-br from-primary/5 to-accent/5", children: _jsxs("div", { className: "flex items-center space-x-3", children: [_jsx("div", { className: "h-9 w-9 rounded-full bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg shadow-primary/25", children: _jsx("span", { className: "text-sm font-bold text-primary-foreground", children: String(context.session.data?.name)?.[0]?.toUpperCase() || '?' }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-medium truncate", children: String(context.session.data?.name) || 'User' }), _jsx("p", { className: "text-xs text-muted-foreground truncate", children: String(context.session.data?.email) || '' })] })] }) }))] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SkeletonLoaderProps {
|
|
2
|
+
className?: string;
|
|
3
|
+
variant?: 'text' | 'circular' | 'rectangular';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Skeleton loader component for content placeholders
|
|
7
|
+
*/
|
|
8
|
+
export declare function SkeletonLoader({ className, variant }: SkeletonLoaderProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
/**
|
|
10
|
+
* Table skeleton loader
|
|
11
|
+
*/
|
|
12
|
+
export declare function TableSkeleton({ rows, columns }: {
|
|
13
|
+
rows?: number;
|
|
14
|
+
columns?: number;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
/**
|
|
17
|
+
* Form skeleton loader
|
|
18
|
+
*/
|
|
19
|
+
export declare function FormSkeleton({ fields }: {
|
|
20
|
+
fields?: number;
|
|
21
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
//# sourceMappingURL=SkeletonLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SkeletonLoader.d.ts","sourceRoot":"","sources":["../../src/components/SkeletonLoader.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,CAAA;CAC9C;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,OAAuB,EAAE,EAAE,mBAAmB,2CAazF;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,EAAE,IAAQ,EAAE,OAAW,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,2CA6B3F;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,EAAE,MAAU,EAAE,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,2CAmB/D"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../lib/utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Skeleton loader component for content placeholders
|
|
5
|
+
*/
|
|
6
|
+
export function SkeletonLoader({ className, variant = 'rectangular' }) {
|
|
7
|
+
const variantClasses = {
|
|
8
|
+
text: 'h-4 rounded',
|
|
9
|
+
circular: 'rounded-full',
|
|
10
|
+
rectangular: 'rounded-md',
|
|
11
|
+
};
|
|
12
|
+
return (_jsx("div", { className: cn('animate-pulse bg-muted', variantClasses[variant], className), "aria-hidden": "true" }));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Table skeleton loader
|
|
16
|
+
*/
|
|
17
|
+
export function TableSkeleton({ rows = 5, columns = 4 }) {
|
|
18
|
+
return (_jsx("div", { className: "bg-card border border-border rounded-lg overflow-hidden", children: _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full", children: [_jsx("thead", { className: "bg-muted/50 border-b border-border", children: _jsx("tr", { children: Array.from({ length: columns }).map((_, i) => (_jsx("th", { className: "px-6 py-3", children: _jsx(SkeletonLoader, { variant: "text", className: "h-4 w-24" }) }, i))) }) }), _jsx("tbody", { className: "divide-y divide-border", children: Array.from({ length: rows }).map((_, rowIndex) => (_jsx("tr", { children: Array.from({ length: columns }).map((_, colIndex) => (_jsx("td", { className: "px-6 py-4", children: _jsx(SkeletonLoader, { variant: "text", className: "h-4 w-32" }) }, colIndex))) }, rowIndex))) })] }) }) }));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Form skeleton loader
|
|
22
|
+
*/
|
|
23
|
+
export function FormSkeleton({ fields = 4 }) {
|
|
24
|
+
return (_jsx("div", { className: "bg-card border border-border rounded-lg p-6", children: _jsxs("div", { className: "space-y-6", children: [Array.from({ length: fields }).map((_, i) => (_jsxs("div", { className: "space-y-2", children: [_jsx(SkeletonLoader, { variant: "text", className: "h-4 w-24" }), _jsx(SkeletonLoader, { variant: "rectangular", className: "h-10 w-full" })] }, i))), _jsx("div", { className: "flex items-center justify-between pt-6 border-t border-border", children: _jsxs("div", { className: "flex gap-3", children: [_jsx(SkeletonLoader, { variant: "rectangular", className: "h-10 w-20" }), _jsx(SkeletonLoader, { variant: "rectangular", className: "h-10 w-20" })] }) })] }) }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface CheckboxFieldProps {
|
|
2
|
+
name: string;
|
|
3
|
+
value: boolean;
|
|
4
|
+
onChange: (value: boolean) => void;
|
|
5
|
+
label: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
mode?: 'read' | 'edit';
|
|
9
|
+
}
|
|
10
|
+
export declare function CheckboxField({ name, value, onChange, label, error, disabled, mode, }: CheckboxFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=CheckboxField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CheckboxField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/CheckboxField.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CACvB;AAED,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,KAAK,EACL,QAAQ,EACR,IAAa,GACd,EAAE,kBAAkB,2CA8BpB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Checkbox } from '../../primitives/checkbox.js';
|
|
4
|
+
import { Label } from '../../primitives/label.js';
|
|
5
|
+
export function CheckboxField({ name, value, onChange, label, error, disabled, mode = 'edit', }) {
|
|
6
|
+
if (mode === 'read') {
|
|
7
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-muted-foreground", children: label }), _jsx("p", { className: "text-sm", children: value ? 'Yes' : 'No' })] }));
|
|
8
|
+
}
|
|
9
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(Checkbox, { id: name, name: name, checked: value || false, onCheckedChange: (checked) => onChange(checked === true), disabled: disabled }), _jsx(Label, { htmlFor: name, className: "leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", children: label })] }), error && _jsx("p", { className: "text-sm text-destructive", children: error })] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ComboboxFieldProps {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string | null;
|
|
4
|
+
onChange: (value: string | null) => void;
|
|
5
|
+
label: string;
|
|
6
|
+
items: Array<{
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}>;
|
|
10
|
+
error?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
mode?: 'read' | 'edit';
|
|
14
|
+
isLoading?: boolean;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function ComboboxField({ name, value, onChange, label, items, error, disabled, required, mode, isLoading, placeholder, }: ComboboxFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=ComboboxField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComboboxField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/ComboboxField.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAa,EACb,SAAiB,EACjB,WAAyB,GAC1B,EAAE,kBAAkB,2CAsFpB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Combobox, ComboboxTrigger, ComboboxContent, ComboboxSearch, ComboboxList, ComboboxEmpty, ComboboxItem, } from '../../primitives/combobox.js';
|
|
5
|
+
export function ComboboxField({ name, value, onChange, label, items, error, disabled, required, mode = 'edit', isLoading = false, placeholder = 'Select...', }) {
|
|
6
|
+
const [open, setOpen] = useState(false);
|
|
7
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
8
|
+
// Read mode
|
|
9
|
+
if (mode === 'read') {
|
|
10
|
+
const selectedItem = items.find((item) => item.id === value);
|
|
11
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx("label", { className: "text-sm font-medium text-muted-foreground", children: label }), _jsx("p", { className: "text-sm", children: selectedItem?.label || '-' })] }));
|
|
12
|
+
}
|
|
13
|
+
// Filter items based on search query
|
|
14
|
+
const filteredItems = searchQuery
|
|
15
|
+
? items.filter((item) => item.label.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
16
|
+
: items;
|
|
17
|
+
const selectedItem = items.find((item) => item.id === value);
|
|
18
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("label", { htmlFor: name, className: "text-sm font-medium", children: [label, required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] }), _jsxs(Combobox, { open: open, onOpenChange: setOpen, children: [_jsx(ComboboxTrigger, { disabled: disabled || isLoading, children: _jsx("span", { className: !selectedItem ? 'text-muted-foreground' : '', children: isLoading ? 'Loading...' : selectedItem ? selectedItem.label : placeholder }) }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxSearch, { placeholder: "Search...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onKeyDown: (e) => {
|
|
19
|
+
// Prevent form submission on Enter
|
|
20
|
+
if (e.key === 'Enter') {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
}
|
|
23
|
+
} }), _jsx(ComboboxList, { children: filteredItems.length === 0 ? (_jsx(ComboboxEmpty, {})) : (_jsxs(_Fragment, { children: [!required && value && (_jsxs(_Fragment, { children: [_jsx(ComboboxItem, { onClick: () => {
|
|
24
|
+
onChange(null);
|
|
25
|
+
setOpen(false);
|
|
26
|
+
setSearchQuery('');
|
|
27
|
+
}, children: _jsx("span", { className: "text-muted-foreground italic", children: "Clear selection" }) }), _jsx("div", { className: "-mx-1 my-1 h-px bg-border" })] })), filteredItems.map((item) => (_jsx(ComboboxItem, { selected: item.id === value, onClick: () => {
|
|
28
|
+
onChange(item.id);
|
|
29
|
+
setOpen(false);
|
|
30
|
+
setSearchQuery('');
|
|
31
|
+
}, children: item.label }, item.id)))] })) })] })] }), error && _jsx("p", { className: "text-sm text-destructive", children: error })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SerializableFieldConfig } from '../../lib/serializeFieldConfig.js';
|
|
2
|
+
export interface FieldRendererProps {
|
|
3
|
+
fieldName: string;
|
|
4
|
+
fieldConfig: SerializableFieldConfig;
|
|
5
|
+
value: unknown;
|
|
6
|
+
onChange: (value: unknown) => void;
|
|
7
|
+
error?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
mode?: 'read' | 'edit';
|
|
10
|
+
relationshipItems?: Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}>;
|
|
14
|
+
relationshipLoading?: boolean;
|
|
15
|
+
basePath?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Factory component that renders the appropriate field type
|
|
19
|
+
* based on the field configuration and component registry
|
|
20
|
+
*/
|
|
21
|
+
export declare function FieldRenderer(props: FieldRendererProps): import("react/jsx-runtime").JSX.Element | null;
|
|
22
|
+
//# sourceMappingURL=FieldRenderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FieldRenderer.d.ts","sourceRoot":"","sources":["../../../src/components/fields/FieldRenderer.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAA;AAEhF,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,uBAAuB,CAAA;IACpC,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,iBAAiB,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxD,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AA6ED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,kDA+BtD"}
|