@opensaas/stack-ui 0.1.7 → 0.4.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 +3 -2
- package/CHANGELOG.md +140 -0
- package/dist/components/AdminUI.d.ts +5 -4
- package/dist/components/AdminUI.d.ts.map +1 -1
- package/dist/components/AdminUI.js +2 -2
- package/dist/components/Dashboard.d.ts +4 -4
- package/dist/components/Dashboard.d.ts.map +1 -1
- package/dist/components/Dashboard.js +4 -3
- package/dist/components/ItemForm.d.ts +4 -4
- package/dist/components/ItemForm.d.ts.map +1 -1
- package/dist/components/ItemForm.js +9 -5
- package/dist/components/ItemFormClient.d.ts.map +1 -1
- package/dist/components/ItemFormClient.js +78 -60
- package/dist/components/ListView.d.ts +4 -4
- package/dist/components/ListView.d.ts.map +1 -1
- package/dist/components/ListView.js +18 -11
- package/dist/components/Navigation.d.ts +5 -4
- package/dist/components/Navigation.d.ts.map +1 -1
- package/dist/components/Navigation.js +3 -2
- package/dist/components/UserMenu.d.ts +11 -0
- package/dist/components/UserMenu.d.ts.map +1 -0
- package/dist/components/UserMenu.js +18 -0
- package/dist/components/fields/TextField.d.ts +2 -1
- package/dist/components/fields/TextField.d.ts.map +1 -1
- package/dist/components/fields/TextField.js +4 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/primitives/button.d.ts +1 -1
- package/dist/primitives/button.d.ts.map +1 -1
- package/dist/styles/globals.css +24 -0
- package/package.json +32 -23
- package/src/components/AdminUI.tsx +8 -5
- package/src/components/Dashboard.tsx +7 -10
- package/src/components/ItemForm.tsx +14 -10
- package/src/components/ItemFormClient.tsx +84 -62
- package/src/components/ListView.tsx +23 -21
- package/src/components/Navigation.tsx +14 -25
- package/src/components/UserMenu.tsx +44 -0
- package/src/components/fields/TextField.tsx +7 -2
- package/src/index.ts +2 -0
- package/src/primitives/button.tsx +1 -2
- package/tests/browser/README.md +154 -0
- package/tests/browser/fields/CheckboxField.browser.test.tsx +245 -0
- package/tests/browser/fields/SelectField.browser.test.tsx +263 -0
- package/tests/browser/fields/TextField.browser.test.tsx +204 -0
- package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-not-be-clickable-when-disabled-1.png +0 -0
- package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-toggle-state-with-multiple-clicks-1.png +0 -0
- package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-handle-special-characters-1.png +0 -0
- package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-support-copy-and-paste-1.png +0 -0
- package/tests/browser/primitives/Button.browser.test.tsx +122 -0
- package/tests/browser/primitives/Dialog.browser.test.tsx +279 -0
- package/tests/browser/primitives/__screenshots__/Button.browser.test.tsx/Button--Browser--should-not-trigger-click-when-disabled-1.png +0 -0
- package/tests/components/CheckboxField.test.tsx +130 -0
- package/tests/components/DeleteButton.test.tsx +331 -0
- package/tests/components/IntegerField.test.tsx +147 -0
- package/tests/components/ListTable.test.tsx +457 -0
- package/tests/components/ListViewClient.test.tsx +415 -0
- package/tests/components/SearchBar.test.tsx +254 -0
- package/tests/components/SelectField.test.tsx +192 -0
- package/vitest.config.ts +20 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
> @opensaas/stack-ui@0.
|
|
2
|
+
> @opensaas/stack-ui@0.4.0 build /home/runner/work/stack/stack/packages/ui
|
|
3
3
|
> tsc && npm run build:css
|
|
4
4
|
|
|
5
5
|
npm warn Unknown env config "verify-deps-before-run". This will stop working in the next major version of npm.
|
|
6
|
+
npm warn Unknown env config "npm-globalconfig". This will stop working in the next major version of npm.
|
|
6
7
|
npm warn Unknown env config "_jsr-registry". This will stop working in the next major version of npm.
|
|
7
8
|
|
|
8
|
-
> @opensaas/stack-ui@0.
|
|
9
|
+
> @opensaas/stack-ui@0.4.0 build:css
|
|
9
10
|
> mkdir -p dist/styles && postcss ./src/styles/globals.css -o ./dist/styles/globals.css
|
|
10
11
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,145 @@
|
|
|
1
1
|
# @opensaas/stack-ui
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#170](https://github.com/OpenSaasAU/stack/pull/170) [`3c4db9d`](https://github.com/OpenSaasAU/stack/commit/3c4db9d8318fc73d291991d8bdfa4f607c3a50ea) Thanks [@list({](https://github.com/list({)! - Add support for virtual fields with proper TypeScript type generation
|
|
8
|
+
|
|
9
|
+
Virtual fields are computed fields that don't exist in the database but are added to query results at runtime. This feature enables derived or computed values to be included in your API responses with full type safety.
|
|
10
|
+
|
|
11
|
+
**New Features:**
|
|
12
|
+
- Added `virtual()` field type for defining computed fields in your schema
|
|
13
|
+
- Virtual fields are automatically excluded from database schema and input types
|
|
14
|
+
- Virtual fields appear in output types with full TypeScript autocomplete
|
|
15
|
+
- Virtual fields support `resolveOutput` hooks for custom computation logic
|
|
16
|
+
|
|
17
|
+
**Type System Improvements:**
|
|
18
|
+
- Generated Context type now properly extends AccessContext from core
|
|
19
|
+
- Separate Input and Output types (e.g., `UserOutput` includes virtual fields, `UserCreateInput` does not)
|
|
20
|
+
- UI components now accept `AccessContext<any>` for better compatibility with custom context types
|
|
21
|
+
- Type aliases provide convenience (e.g., `User = UserOutput`)
|
|
22
|
+
|
|
23
|
+
**Example Usage:**
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { list, text, virtual } from '@opensaas/stack-core'
|
|
27
|
+
|
|
28
|
+
export default config({
|
|
29
|
+
lists: {
|
|
30
|
+
|
|
31
|
+
fields: {
|
|
32
|
+
name: text(),
|
|
33
|
+
email: text(),
|
|
34
|
+
displayName: virtual({
|
|
35
|
+
type: 'string',
|
|
36
|
+
hooks: {
|
|
37
|
+
resolveOutput: async ({ item }) => {
|
|
38
|
+
return `${item.name} (${item.email})`
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `displayName` field will automatically appear in query results with full TypeScript support, but won't be part of create/update operations or the database schema.
|
|
49
|
+
|
|
50
|
+
### Patch Changes
|
|
51
|
+
|
|
52
|
+
- [#172](https://github.com/OpenSaasAU/stack/pull/172) [`929a2a9`](https://github.com/OpenSaasAU/stack/commit/929a2a9a2dfa80b1d973d259dd87828d644ea58d) Thanks [@list<Lists.User.TypeInfo>({](https://github.com/list<Lists.User.TypeInfo>({), [@list<Lists.User.TypeInfo>({](https://github.com/list<Lists.User.TypeInfo>({)! - Improve TypeScript type inference for field configs and list-level hooks by automatically passing TypeInfo from list level down
|
|
53
|
+
|
|
54
|
+
This change eliminates the need to manually specify type parameters on field builders when using features like virtual fields, and fixes a critical bug where list-level hooks weren't receiving properly typed parameters.
|
|
55
|
+
|
|
56
|
+
## Field Type Inference Improvements
|
|
57
|
+
|
|
58
|
+
Previously, users had to write `virtual<Lists.User.TypeInfo>({...})` to get proper type inference. Now TypeScript automatically infers the correct types from the list-level type parameter.
|
|
59
|
+
|
|
60
|
+
**Example:**
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Before
|
|
64
|
+
|
|
65
|
+
fields: {
|
|
66
|
+
displayName: virtual<Lists.User.TypeInfo>({
|
|
67
|
+
type: 'string',
|
|
68
|
+
hooks: {
|
|
69
|
+
resolveOutput: ({ item }) => `${item.name} (${item.email})`,
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// After
|
|
76
|
+
|
|
77
|
+
fields: {
|
|
78
|
+
displayName: virtual({
|
|
79
|
+
type: 'string',
|
|
80
|
+
hooks: {
|
|
81
|
+
resolveOutput: ({ item }) => `${item.name} (${item.email})`,
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## List-Level Hooks Type Inference Fix
|
|
89
|
+
|
|
90
|
+
Fixed a critical type parameter mismatch where `Hooks<TTypeInfo>` was passing the entire TypeInfo object as the first parameter instead of properly destructuring it into three required parameters:
|
|
91
|
+
1. `TOutput` - The item type (what's stored in DB)
|
|
92
|
+
2. `TCreateInput` - Prisma create input type
|
|
93
|
+
3. `TUpdateInput` - Prisma update input type
|
|
94
|
+
|
|
95
|
+
**Impact:**
|
|
96
|
+
- `resolveInput` now receives proper Prisma input types (e.g., `PostCreateInput`, `PostUpdateInput`)
|
|
97
|
+
- `validateInput` has access to properly typed input data
|
|
98
|
+
- `beforeOperation` and `afterOperation` have correct item types
|
|
99
|
+
- All list-level hook callbacks now get full IntelliSense and type checking
|
|
100
|
+
|
|
101
|
+
**Example:**
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
Post: list<Lists.Post.TypeInfo>({
|
|
105
|
+
fields: { title: text(), content: text() },
|
|
106
|
+
hooks: {
|
|
107
|
+
resolveInput: async ({ operation, resolvedData }) => {
|
|
108
|
+
// ✅ resolvedData is now properly typed as PostCreateInput or PostUpdateInput
|
|
109
|
+
// ✅ Full autocomplete for title, content, etc.
|
|
110
|
+
if (operation === 'create') {
|
|
111
|
+
console.log(resolvedData.title) // TypeScript knows this is string | undefined
|
|
112
|
+
}
|
|
113
|
+
return resolvedData
|
|
114
|
+
},
|
|
115
|
+
beforeOperation: async ({ operation, item }) => {
|
|
116
|
+
// ✅ item is now properly typed as Post with all fields
|
|
117
|
+
if (operation === 'update' && item) {
|
|
118
|
+
console.log(item.title) // TypeScript knows this is string
|
|
119
|
+
console.log(item.createdAt) // TypeScript knows this is Date
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Breaking Changes
|
|
127
|
+
- Field types now accept full `TTypeInfo extends TypeInfo` instead of just `TItem`
|
|
128
|
+
- `FieldsWithItemType` utility replaced with `FieldsWithTypeInfo`
|
|
129
|
+
- All field builders updated to use new type signature
|
|
130
|
+
- List-level hooks now receive properly typed parameters (may reveal existing type errors)
|
|
131
|
+
|
|
132
|
+
## Benefits
|
|
133
|
+
- ✨ Cleaner code without manual type parameter repetition
|
|
134
|
+
- 🎯 Better type inference in both field-level and list-level hooks
|
|
135
|
+
- 🔄 Consistent type flow from list configuration down to individual fields
|
|
136
|
+
- 🛡️ Maintained full type safety with improved DX
|
|
137
|
+
- 💡 Full IntelliSense support in all hook callbacks
|
|
138
|
+
|
|
139
|
+
## 0.3.0
|
|
140
|
+
|
|
141
|
+
## 0.2.0
|
|
142
|
+
|
|
3
143
|
## 0.1.7
|
|
4
144
|
|
|
5
145
|
### Patch Changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ServerActionInput } from '../server/types.js';
|
|
2
|
-
import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
3
|
-
export interface AdminUIProps
|
|
4
|
-
context: AccessContext<
|
|
2
|
+
import { type AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
3
|
+
export interface AdminUIProps {
|
|
4
|
+
context: AccessContext<unknown>;
|
|
5
5
|
config: OpenSaasConfig;
|
|
6
6
|
params?: string[];
|
|
7
7
|
searchParams?: {
|
|
@@ -9,6 +9,7 @@ export interface AdminUIProps<TPrisma> {
|
|
|
9
9
|
};
|
|
10
10
|
basePath?: string;
|
|
11
11
|
serverAction: (input: ServerActionInput) => Promise<unknown>;
|
|
12
|
+
onSignOut?: () => Promise<void>;
|
|
12
13
|
}
|
|
13
14
|
/**
|
|
14
15
|
* Main AdminUI component - complete admin interface with routing
|
|
@@ -20,5 +21,5 @@ export interface AdminUIProps<TPrisma> {
|
|
|
20
21
|
* - [list, 'create'] → ItemForm (create)
|
|
21
22
|
* - [list, id] → ItemForm (edit)
|
|
22
23
|
*/
|
|
23
|
-
export declare function AdminUI
|
|
24
|
+
export declare function AdminUI({ context, config, params, searchParams, basePath, serverAction, onSignOut, }: AdminUIProps): import("react/jsx-runtime").JSX.Element;
|
|
24
25
|
//# sourceMappingURL=AdminUI.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminUI.d.ts","sourceRoot":"","sources":["../../src/components/AdminUI.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAqB,cAAc,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"AdminUI.d.ts","sourceRoot":"","sources":["../../src/components/AdminUI.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,KAAK,aAAa,EAAqB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAG5F,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,YAAY,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5D,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,EACtB,OAAO,EACP,MAAM,EACN,MAAW,EACX,YAAiB,EACjB,QAAmB,EACnB,YAAY,EACZ,SAAS,GACV,EAAE,YAAY,2CA4Ed"}
|
|
@@ -15,7 +15,7 @@ import { generateThemeCSS } from '../lib/theme.js';
|
|
|
15
15
|
* - [list, 'create'] → ItemForm (create)
|
|
16
16
|
* - [list, id] → ItemForm (edit)
|
|
17
17
|
*/
|
|
18
|
-
export function AdminUI({ context, config, params = [], searchParams = {}, basePath = '/admin', serverAction, }) {
|
|
18
|
+
export function AdminUI({ context, config, params = [], searchParams = {}, basePath = '/admin', serverAction, onSignOut, }) {
|
|
19
19
|
// Parse route from params
|
|
20
20
|
const [urlSegment, action] = params;
|
|
21
21
|
// Convert URL segment (kebab-case) to PascalCase listKey
|
|
@@ -44,5 +44,5 @@ export function AdminUI({ context, config, params = [], searchParams = {}, baseP
|
|
|
44
44
|
}
|
|
45
45
|
// Generate theme styles if custom theme is configured
|
|
46
46
|
const themeStyles = config.ui?.theme ? generateThemeCSS(config.ui.theme) : null;
|
|
47
|
-
return (_jsxs(_Fragment, { children: [themeStyles && _jsx("style", { dangerouslySetInnerHTML: { __html: themeStyles } }), _jsxs("div", { className: "flex min-h-screen bg-background", children: [_jsx(Navigation, { context: context, config: config, basePath: basePath, currentPath: currentPath }), _jsx("main", { className: "flex-1 overflow-y-auto", children: content })] })] }));
|
|
47
|
+
return (_jsxs(_Fragment, { children: [themeStyles && _jsx("style", { dangerouslySetInnerHTML: { __html: themeStyles } }), _jsxs("div", { className: "flex min-h-screen bg-background", children: [_jsx(Navigation, { context: context, config: config, basePath: basePath, currentPath: currentPath, onSignOut: onSignOut }), _jsx("main", { className: "flex-1 overflow-y-auto", children: content })] })] }));
|
|
48
48
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
2
|
-
export interface DashboardProps
|
|
3
|
-
context: AccessContext<
|
|
1
|
+
import { type AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
2
|
+
export interface DashboardProps {
|
|
3
|
+
context: AccessContext<unknown>;
|
|
4
4
|
config: OpenSaasConfig;
|
|
5
5
|
basePath?: string;
|
|
6
6
|
}
|
|
@@ -8,5 +8,5 @@ export interface DashboardProps<TPrisma> {
|
|
|
8
8
|
* Dashboard landing page showing all available lists
|
|
9
9
|
* Server Component
|
|
10
10
|
*/
|
|
11
|
-
export declare function Dashboard
|
|
11
|
+
export declare function Dashboard({ context, config, basePath }: DashboardProps): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
12
12
|
//# sourceMappingURL=Dashboard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/components/Dashboard.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/components/Dashboard.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAG9F,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,QAAmB,EAAE,EAAE,cAAc,oDAmHvF"}
|
|
@@ -7,13 +7,14 @@ import { Card, CardContent, CardHeader, CardTitle } from '../primitives/card.js'
|
|
|
7
7
|
* Dashboard landing page showing all available lists
|
|
8
8
|
* Server Component
|
|
9
9
|
*/
|
|
10
|
-
export async function Dashboard({ context, config, basePath = '/admin'
|
|
10
|
+
export async function Dashboard({ context, config, basePath = '/admin' }) {
|
|
11
11
|
const lists = Object.keys(config.lists || {});
|
|
12
12
|
// Get counts for each list
|
|
13
13
|
const listCounts = await Promise.all(lists.map(async (listKey) => {
|
|
14
14
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
15
|
+
const delegate = context.db[getDbKey(listKey)];
|
|
16
|
+
const count = delegate?.count ? await delegate.count() : 0;
|
|
17
|
+
return { listKey, count };
|
|
17
18
|
}
|
|
18
19
|
catch (error) {
|
|
19
20
|
console.error(`Failed to get count for ${listKey}:`, error);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ServerActionInput } from '../server/types.js';
|
|
2
|
-
import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
3
|
-
export interface ItemFormProps
|
|
4
|
-
context: AccessContext<
|
|
2
|
+
import { type AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
3
|
+
export interface ItemFormProps {
|
|
4
|
+
context: AccessContext<unknown>;
|
|
5
5
|
config: OpenSaasConfig;
|
|
6
6
|
listKey: string;
|
|
7
7
|
mode: 'create' | 'edit';
|
|
@@ -13,5 +13,5 @@ export interface ItemFormProps<TPrisma> {
|
|
|
13
13
|
* Item form component - create or edit an item
|
|
14
14
|
* Server Component that fetches data and sets up actions
|
|
15
15
|
*/
|
|
16
|
-
export declare function ItemForm
|
|
16
|
+
export declare function ItemForm({ context, config, listKey, mode, itemId, basePath, serverAction, }: ItemFormProps): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
17
17
|
//# sourceMappingURL=ItemForm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ItemForm.d.ts","sourceRoot":"","sources":["../../src/components/ItemForm.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"ItemForm.d.ts","sourceRoot":"","sources":["../../src/components/ItemForm.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,KAAK,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAG9F,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC7D;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,EAC7B,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,EACJ,MAAM,EACN,QAAmB,EACnB,YAAY,GACb,EAAE,aAAa,oDAuKf"}
|
|
@@ -27,10 +27,13 @@ export async function ItemForm({ context, config, listKey, mode, itemId, basePat
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
// Fetch item with relationships included
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const delegate = context.db[getDbKey(listKey)];
|
|
31
|
+
if (delegate?.findUnique) {
|
|
32
|
+
itemData = await delegate.findUnique({
|
|
33
|
+
where: { id: itemId },
|
|
34
|
+
...(Object.keys(includeRelationships).length > 0 && { include: includeRelationships }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
34
37
|
}
|
|
35
38
|
catch (error) {
|
|
36
39
|
console.error(`Failed to fetch item ${itemId}:`, error);
|
|
@@ -53,7 +56,8 @@ export async function ItemForm({ context, config, listKey, mode, itemId, basePat
|
|
|
53
56
|
if (relatedListConfig) {
|
|
54
57
|
try {
|
|
55
58
|
const dbContext = context.db;
|
|
56
|
-
const
|
|
59
|
+
const delegate = dbContext[getDbKey(relatedListName)];
|
|
60
|
+
const relatedItems = delegate?.findMany ? await delegate.findMany({}) : [];
|
|
57
61
|
// Use 'name' field as label if it exists, otherwise use 'id'
|
|
58
62
|
relationshipData[fieldName] = relatedItems.map((item) => ({
|
|
59
63
|
id: item.id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ItemFormClient.d.ts","sourceRoot":"","sources":["../../src/components/ItemFormClient.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AAE7E,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAA;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CACxE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,MAAM,EACN,IAAI,EACJ,MAAM,EACN,WAAgB,EAChB,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,gBAAqB,GACtB,EAAE,mBAAmB,
|
|
1
|
+
{"version":3,"file":"ItemFormClient.d.ts","sourceRoot":"","sources":["../../src/components/ItemFormClient.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AAE7E,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAA;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CACxE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,MAAM,EACN,IAAI,EACJ,MAAM,EACN,WAAgB,EAChB,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,gBAAqB,GACtB,EAAE,mBAAmB,2CA8NrB"}
|
|
@@ -33,67 +33,78 @@ export function ItemFormClient({ listKey, urlKey, mode, fields, initialData = {}
|
|
|
33
33
|
setErrors({});
|
|
34
34
|
setGeneralError(null);
|
|
35
35
|
startTransition(async () => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (fieldAny
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
// Single relationship: use connect format
|
|
60
|
-
if (value) {
|
|
61
|
-
transformedData[fieldName] = {
|
|
62
|
-
connect: { id: value },
|
|
63
|
-
};
|
|
64
|
-
}
|
|
36
|
+
// Transform relationship fields to Prisma format
|
|
37
|
+
// Filter out password fields with isSet objects (unchanged passwords)
|
|
38
|
+
// File/Image fields: pass File objects through (Next.js will serialize them)
|
|
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
|
+
};
|
|
65
55
|
}
|
|
66
56
|
}
|
|
67
57
|
else {
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
58
|
+
// Single relationship: use connect format
|
|
59
|
+
if (value) {
|
|
60
|
+
transformedData[fieldName] = {
|
|
61
|
+
connect: { id: value },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
71
64
|
}
|
|
72
65
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
66
|
+
else {
|
|
67
|
+
// Non-relationship field: pass through (including File objects for file/image fields)
|
|
68
|
+
// File objects will be serialized by Next.js server action
|
|
69
|
+
transformedData[fieldName] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const result = mode === 'create'
|
|
73
|
+
? await serverAction({
|
|
74
|
+
listKey,
|
|
75
|
+
action: 'create',
|
|
76
|
+
data: transformedData,
|
|
77
|
+
})
|
|
78
|
+
: await serverAction({
|
|
79
|
+
listKey,
|
|
80
|
+
action: 'update',
|
|
81
|
+
id: itemId,
|
|
82
|
+
data: transformedData,
|
|
83
|
+
});
|
|
84
|
+
// Check if result has the new format with success/error fields
|
|
85
|
+
if (result && typeof result === 'object' && 'success' in result) {
|
|
86
|
+
const actionResult = result;
|
|
87
|
+
if (actionResult.success) {
|
|
86
88
|
// Navigate back to list view
|
|
87
89
|
router.push(`${basePath}/${urlKey}`);
|
|
88
90
|
router.refresh();
|
|
89
91
|
}
|
|
90
92
|
else {
|
|
91
|
-
|
|
93
|
+
// Handle error response
|
|
94
|
+
if (actionResult.fieldErrors) {
|
|
95
|
+
setErrors(actionResult.fieldErrors);
|
|
96
|
+
}
|
|
97
|
+
setGeneralError(actionResult.error);
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
else if (result) {
|
|
101
|
+
// Legacy format: result is the data itself
|
|
102
|
+
router.push(`${basePath}/${urlKey}`);
|
|
103
|
+
router.refresh();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// null result means access denied
|
|
107
|
+
setGeneralError('Access denied or operation failed');
|
|
97
108
|
}
|
|
98
109
|
});
|
|
99
110
|
};
|
|
@@ -103,23 +114,30 @@ export function ItemFormClient({ listKey, urlKey, mode, fields, initialData = {}
|
|
|
103
114
|
setGeneralError(null);
|
|
104
115
|
setShowDeleteConfirm(false);
|
|
105
116
|
startTransition(async () => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
const result = await serverAction({
|
|
118
|
+
listKey,
|
|
119
|
+
action: 'delete',
|
|
120
|
+
id: itemId,
|
|
121
|
+
});
|
|
122
|
+
// Check if result has the new format with success/error fields
|
|
123
|
+
if (result && typeof result === 'object' && 'success' in result) {
|
|
124
|
+
const actionResult = result;
|
|
125
|
+
if (actionResult.success) {
|
|
113
126
|
router.push(`${basePath}/${urlKey}`);
|
|
114
127
|
router.refresh();
|
|
115
128
|
}
|
|
116
129
|
else {
|
|
117
|
-
setGeneralError(
|
|
130
|
+
setGeneralError(actionResult.error);
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
else if (result) {
|
|
134
|
+
// Legacy format: result is the data itself
|
|
135
|
+
router.push(`${basePath}/${urlKey}`);
|
|
136
|
+
router.refresh();
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// null result means access denied
|
|
140
|
+
setGeneralError('Access denied or failed to delete item');
|
|
123
141
|
}
|
|
124
142
|
});
|
|
125
143
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AccessContext, OpenSaasConfig
|
|
2
|
-
export interface ListViewProps
|
|
3
|
-
context: AccessContext<
|
|
1
|
+
import { type AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
2
|
+
export interface ListViewProps {
|
|
3
|
+
context: AccessContext<unknown>;
|
|
4
4
|
config: OpenSaasConfig;
|
|
5
5
|
listKey: string;
|
|
6
6
|
basePath?: string;
|
|
@@ -13,5 +13,5 @@ export interface ListViewProps<TPrisma extends PrismaClientLike = PrismaClientLi
|
|
|
13
13
|
* List view component - displays items in a table
|
|
14
14
|
* Server Component that fetches data and renders client table
|
|
15
15
|
*/
|
|
16
|
-
export declare function ListView
|
|
16
|
+
export declare function ListView({ context, config, listKey, basePath, columns, page, pageSize, search, }: ListViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
17
17
|
//# sourceMappingURL=ListView.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ListView.d.ts","sourceRoot":"","sources":["../../src/components/ListView.tsx"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"ListView.d.ts","sourceRoot":"","sources":["../../src/components/ListView.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE9F,MAAM,WAAW,aAAa;IAC5B,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,EAC7B,OAAO,EACP,MAAM,EACN,OAAO,EACP,QAAmB,EACnB,OAAO,EACP,IAAQ,EACR,QAAa,EACb,MAAM,GACP,EAAE,aAAa,oDA6Hf"}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import Link from 'next/link.js';
|
|
3
3
|
import { ListViewClient } from './ListViewClient.js';
|
|
4
4
|
import { formatListName } from '../lib/utils.js';
|
|
5
|
-
import { getDbKey, getUrlKey
|
|
5
|
+
import { getDbKey, getUrlKey } from '@opensaas/stack-core';
|
|
6
6
|
/**
|
|
7
7
|
* List view component - displays items in a table
|
|
8
8
|
* Server Component that fetches data and renders client table
|
|
@@ -47,15 +47,19 @@ export async function ListView({ context, config, listKey, basePath = '/admin',
|
|
|
47
47
|
include[fieldName] = true;
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
const delegate = dbContext[key];
|
|
51
|
+
if (delegate?.findMany && delegate?.count) {
|
|
52
|
+
;
|
|
53
|
+
[items, total] = await Promise.all([
|
|
54
|
+
delegate.findMany({
|
|
55
|
+
where,
|
|
56
|
+
skip,
|
|
57
|
+
take: pageSize,
|
|
58
|
+
...(Object.keys(include).length > 0 ? { include } : {}),
|
|
59
|
+
}),
|
|
60
|
+
delegate.count({ where }),
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
catch (error) {
|
|
61
65
|
console.error(`Failed to fetch ${listKey}:`, error);
|
|
@@ -65,7 +69,10 @@ export async function ListView({ context, config, listKey, basePath = '/admin',
|
|
|
65
69
|
// Extract only the relationship refs needed by client (don't send entire config)
|
|
66
70
|
const relationshipRefs = {};
|
|
67
71
|
Object.entries(listConfig.fields).forEach(([fieldName, field]) => {
|
|
68
|
-
if ('type' in field &&
|
|
72
|
+
if ('type' in field &&
|
|
73
|
+
field.type === 'relationship' &&
|
|
74
|
+
'ref' in field &&
|
|
75
|
+
typeof field.ref === 'string') {
|
|
69
76
|
relationshipRefs[fieldName] = field.ref;
|
|
70
77
|
}
|
|
71
78
|
});
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
2
|
-
export interface NavigationProps
|
|
3
|
-
context: AccessContext<
|
|
1
|
+
import { type AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
2
|
+
export interface NavigationProps {
|
|
3
|
+
context: AccessContext<unknown>;
|
|
4
4
|
config: OpenSaasConfig;
|
|
5
5
|
basePath?: string;
|
|
6
6
|
currentPath?: string;
|
|
7
|
+
onSignOut?: () => Promise<void>;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* Navigation sidebar showing all lists
|
|
10
11
|
* Server Component
|
|
11
12
|
*/
|
|
12
|
-
export declare function Navigation
|
|
13
|
+
export declare function Navigation({ context, config, basePath, currentPath, onSignOut, }: NavigationProps): import("react/jsx-runtime").JSX.Element;
|
|
13
14
|
//# sourceMappingURL=Navigation.d.ts.map
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"Navigation.d.ts","sourceRoot":"","sources":["../../src/components/Navigation.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAa,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGpF,MAAM,WAAW,eAAe;IAC9B,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;IACpB,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAChC;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,MAAM,EACN,QAAmB,EACnB,WAAgB,EAChB,SAAS,GACV,EAAE,eAAe,2CAkFjB"}
|
|
@@ -2,11 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import Link from 'next/link.js';
|
|
3
3
|
import { formatListName } from '../lib/utils.js';
|
|
4
4
|
import { getUrlKey } from '@opensaas/stack-core';
|
|
5
|
+
import { UserMenu } from './UserMenu.js';
|
|
5
6
|
/**
|
|
6
7
|
* Navigation sidebar showing all lists
|
|
7
8
|
* Server Component
|
|
8
9
|
*/
|
|
9
|
-
export function Navigation({ context, config, basePath = '/admin', currentPath = '', }) {
|
|
10
|
+
export function Navigation({ context, config, basePath = '/admin', currentPath = '', onSignOut, }) {
|
|
10
11
|
const lists = Object.keys(config.lists || {});
|
|
11
12
|
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
13
|
? 'bg-gradient-to-r from-primary to-accent text-primary-foreground shadow-lg shadow-primary/25'
|
|
@@ -16,5 +17,5 @@ export function Navigation({ context, config, basePath = '/admin', currentPath =
|
|
|
16
17
|
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
18
|
? 'bg-gradient-to-r from-primary to-accent text-primary-foreground shadow-lg shadow-primary/25'
|
|
18
19
|
: '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(
|
|
20
|
+
})] }))] }) }), context.session && (_jsx(UserMenu, { userName: String(context.session.data?.name) || 'User', userEmail: String(context.session.data?.email) || '', onSignOut: onSignOut }))] }));
|
|
20
21
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface UserMenuProps {
|
|
2
|
+
userName?: string;
|
|
3
|
+
userEmail?: string;
|
|
4
|
+
onSignOut?: () => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* User menu component with sign-out button
|
|
8
|
+
* Client Component
|
|
9
|
+
*/
|
|
10
|
+
export declare function UserMenu({ userName, userEmail, onSignOut }: UserMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=UserMenu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserMenu.d.ts","sourceRoot":"","sources":["../../src/components/UserMenu.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAChC;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,aAAa,2CA4BzE"}
|