@nocobase/app-template-default 1.0.0-beta.86 → 1.0.0-beta.88
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/.agents/rules/notification-templates.md +5 -0
- package/.agents/rules/shell.md +4 -2
- package/AGENTS.md +3 -1
- package/README.MD +11 -5
- package/client/components/body-portal.tsx +25 -0
- package/client/components/breadcrumbs.tsx +13 -0
- package/client/components/flow-diagram.tsx +2 -2
- package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +1 -1
- package/client/layouts/app-layout.tsx +8 -4
- package/client/layouts/components/header-actions.tsx +0 -2
- package/client/layouts/components/sidebar-footer.tsx +18 -25
- package/client/layouts/components/user-menu.tsx +6 -13
- package/client/lib/navigation-transform.ts +156 -22
- package/client/locales/system/en-US.ts +8 -6
- package/client/locales/system/settings-en-US.ts +17 -0
- package/client/locales/system/settings-zh-CN.ts +15 -0
- package/client/locales/system/zh-CN.ts +8 -6
- package/client/pages/auth/register.tsx +39 -22
- package/client/pages/auth/shared.tsx +1 -3
- package/client/pages/settings/hooks.tsx +3 -68
- package/client/pages/settings/notification-routes.ts +109 -0
- package/client/pages/settings/notification-templates.tsx +26 -0
- package/client/pages/settings/permission-management-page.tsx +15 -90
- package/client/pages/settings/routes.ts +96 -98
- package/client/pages/settings/settings-home.tsx +109 -0
- package/client/pages/settings/settings-module-page.tsx +158 -0
- package/client/pages/settings/shell-routes.ts +53 -0
- package/client/routes.ts +17 -1
- package/client/routing/app-router.tsx +1 -14
- package/client/routing/client-route.tsx +16 -9
- package/client/routing/route-context.tsx +30 -3
- package/client/routing/route-leave-boundary.tsx +249 -0
- package/client/routing/route-navigation.ts +72 -8
- package/client/routing/route-tree.tsx +7 -12
- package/client/routing/standalone-page-layout.tsx +0 -5
- package/client/runtime.ts +2 -2
- package/client/theme/theme-settings.tsx +60 -162
- package/config.example.yml +51 -32
- package/package.json +28 -28
- package/server/config/auth.ts +2 -2
- package/server/config/index.ts +3 -0
- package/server/config/mailers.ts +11 -0
- package/server/providers/index.ts +2 -0
- package/server/providers/notification-management.ts +27 -0
- package/tests/client/account-permissions.test.tsx +1 -1
- package/tests/client/client-auth.test.tsx +10 -1
- package/tests/client/client-routes.test.ts +137 -4
- package/tests/client/client-shell.test.tsx +29 -10
- package/tests/client/client-theme.test.tsx +118 -39
- package/tests/client/foundation-integration.test.ts +38 -17
- package/tests/client/header-hover.test.tsx +33 -5
- package/tests/client/layout-i18n.test.tsx +9 -4
- package/tests/client/permission-management-tabs.test.tsx +421 -16
- package/tests/client/registration-feedback.test.tsx +143 -0
- package/tests/client/settings-home.test.tsx +110 -0
- package/tests/client/settings-navigation.test.ts +230 -0
- package/tests/client/standalone-page-layout.test.tsx +45 -0
- package/tests/client/user-menu-sign-out.test.tsx +38 -14
- package/tests/client/user-menu.test.tsx +65 -0
- package/tests/fixtures/theme-tokens.tsx +13 -1
- package/tests/server/auth-email-config.test.ts +51 -17
- package/tests/server/notification-templates.test.ts +144 -0
- package/tests/client/permission-management-routes.test.ts +0 -64
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Monitor, Moon, Palette, Sun } from 'lucide-react';
|
|
2
2
|
import { useTheme } from 'next-themes';
|
|
3
3
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
4
|
-
import {
|
|
5
|
-
import { Button } from '#components/ui/button';
|
|
6
|
-
import { RadioGroup, RadioGroupItem } from '#components/ui/radio-group';
|
|
4
|
+
import type { ReactElement } from 'react';
|
|
7
5
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
DropdownMenuLabel,
|
|
7
|
+
DropdownMenuRadioGroup,
|
|
8
|
+
DropdownMenuRadioItem,
|
|
9
|
+
DropdownMenuSeparator,
|
|
10
|
+
DropdownMenuSub,
|
|
11
|
+
DropdownMenuSubContent,
|
|
12
|
+
DropdownMenuSubTrigger,
|
|
13
|
+
} from '#components/ui/dropdown-menu';
|
|
14
14
|
import { useThemePreset } from './theme-context';
|
|
15
15
|
import { themePresets } from './theme-presets';
|
|
16
16
|
|
|
@@ -20,164 +20,62 @@ const COLOR_MODES = [
|
|
|
20
20
|
{ id: 'system', icon: Monitor },
|
|
21
21
|
] as const;
|
|
22
22
|
|
|
23
|
+
/** Account-menu appearance choices; the existing providers own persistence. */
|
|
23
24
|
export function ThemeSettings(): ReactElement {
|
|
24
25
|
const { theme, setTheme } = useTheme();
|
|
25
26
|
const { preset, setPreset } = useThemePreset();
|
|
26
27
|
const { t } = useTranslation();
|
|
27
|
-
const id = useId();
|
|
28
|
-
const title = t('appearance.title', { defaultValue: 'Appearance' });
|
|
29
|
-
return (
|
|
30
|
-
<Popover>
|
|
31
|
-
<PopoverTrigger
|
|
32
|
-
openOnHover
|
|
33
|
-
delay={0}
|
|
34
|
-
render={
|
|
35
|
-
<Button
|
|
36
|
-
aria-label={title}
|
|
37
|
-
size='icon'
|
|
38
|
-
variant='outline'
|
|
39
|
-
className='size-10 rounded-xl border-border bg-background/60 hover:bg-accent/50'
|
|
40
|
-
/>
|
|
41
|
-
}
|
|
42
|
-
>
|
|
43
|
-
<Palette aria-hidden='true' className='size-5' />
|
|
44
|
-
</PopoverTrigger>
|
|
45
|
-
{/* Keep the panel width independent of density; its content still uses theme tokens. */}
|
|
46
|
-
<PopoverContent
|
|
47
|
-
align='end'
|
|
48
|
-
className='w-xs max-w-[calc(100vw-2rem)] gap-4 p-4'
|
|
49
|
-
>
|
|
50
|
-
<div className='flex items-center justify-between'>
|
|
51
|
-
<PopoverTitle className='text-sm font-semibold text-foreground'>
|
|
52
|
-
{title}
|
|
53
|
-
</PopoverTitle>
|
|
54
|
-
</div>
|
|
55
28
|
|
|
56
|
-
|
|
57
|
-
|
|
29
|
+
return (
|
|
30
|
+
<DropdownMenuSub>
|
|
31
|
+
<DropdownMenuSubTrigger className='gap-2'>
|
|
32
|
+
<Palette aria-hidden='true' className='size-4' />
|
|
33
|
+
{t('appearance.title', { defaultValue: 'Theme' })}
|
|
34
|
+
</DropdownMenuSubTrigger>
|
|
35
|
+
<DropdownMenuSubContent className='min-w-44'>
|
|
36
|
+
<DropdownMenuRadioGroup
|
|
37
|
+
value={theme}
|
|
38
|
+
onValueChange={(value) => setTheme(String(value))}
|
|
39
|
+
aria-label={t('appearance.mode', { defaultValue: 'Color mode' })}
|
|
40
|
+
>
|
|
41
|
+
<DropdownMenuLabel>
|
|
58
42
|
{t('appearance.mode', { defaultValue: 'Color mode' })}
|
|
59
|
-
</
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{/* `absolute` over the primitive's `relative`, which would keep
|
|
80
|
-
the hidden radio in the row and push the words aside. */}
|
|
81
|
-
<RadioGroupItem value={mode} className='absolute sr-only' />
|
|
82
|
-
<ModeIcon aria-hidden='true' className='size-3.5 shrink-0' />
|
|
83
|
-
<span className='whitespace-nowrap'>
|
|
84
|
-
{t('appearance.' + mode, {
|
|
85
|
-
defaultValue: mode[0].toUpperCase() + mode.slice(1),
|
|
86
|
-
})}
|
|
87
|
-
</span>
|
|
88
|
-
</label>
|
|
89
|
-
);
|
|
90
|
-
})}
|
|
91
|
-
</RadioGroup>
|
|
92
|
-
</fieldset>
|
|
93
|
-
|
|
94
|
-
<fieldset className='space-y-2'>
|
|
95
|
-
<legend className='text-xs font-medium text-muted-foreground'>
|
|
43
|
+
</DropdownMenuLabel>
|
|
44
|
+
{COLOR_MODES.map(({ id: mode, icon: ModeIcon }) => (
|
|
45
|
+
<DropdownMenuRadioItem key={mode} value={mode} closeOnClick={false}>
|
|
46
|
+
<ModeIcon aria-hidden='true' className='size-4' />
|
|
47
|
+
{t('appearance.' + mode, {
|
|
48
|
+
defaultValue: mode[0].toUpperCase() + mode.slice(1),
|
|
49
|
+
})}
|
|
50
|
+
</DropdownMenuRadioItem>
|
|
51
|
+
))}
|
|
52
|
+
</DropdownMenuRadioGroup>
|
|
53
|
+
<DropdownMenuSeparator />
|
|
54
|
+
<DropdownMenuRadioGroup
|
|
55
|
+
value={preset}
|
|
56
|
+
onValueChange={(value) => {
|
|
57
|
+
const selected = themePresets.find((item) => item.id === value);
|
|
58
|
+
if (selected) setPreset(selected.id);
|
|
59
|
+
}}
|
|
60
|
+
aria-label={t('appearance.preset', { defaultValue: 'Theme' })}
|
|
61
|
+
>
|
|
62
|
+
<DropdownMenuLabel>
|
|
96
63
|
{t('appearance.preset', { defaultValue: 'Theme' })}
|
|
97
|
-
</
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
className={cn(
|
|
114
|
-
'group relative flex cursor-pointer flex-col rounded-xl border p-2 text-left transition-all select-none',
|
|
115
|
-
isSelected
|
|
116
|
-
? 'border-primary ring-2 ring-primary/20 bg-primary/5 shadow-xs'
|
|
117
|
-
: 'border-border hover:bg-muted/30',
|
|
118
|
-
)}
|
|
119
|
-
>
|
|
120
|
-
<RadioGroupItem
|
|
121
|
-
value={item.id}
|
|
122
|
-
className='absolute sr-only'
|
|
123
|
-
/>
|
|
124
|
-
<div className='mb-2 h-16 w-full'>
|
|
125
|
-
<div
|
|
126
|
-
aria-hidden='true'
|
|
127
|
-
data-theme={item.id}
|
|
128
|
-
className='theme-preview relative flex h-full overflow-hidden rounded-lg border border-border bg-background shadow-2xs'
|
|
129
|
-
>
|
|
130
|
-
{/* Mini Sidebar */}
|
|
131
|
-
<div className='flex w-6 flex-col gap-1 border-r border-sidebar-border/50 bg-sidebar p-1'>
|
|
132
|
-
<div className='size-2 rounded-xs bg-sidebar-primary-foreground' />
|
|
133
|
-
<div className='h-1 w-full rounded-xs bg-sidebar-foreground/30' />
|
|
134
|
-
<div className='h-1 w-full rounded-xs bg-sidebar-foreground/20' />
|
|
135
|
-
</div>
|
|
136
|
-
{/* Mini Main Content Area */}
|
|
137
|
-
<div className='flex flex-1 flex-col p-1.5'>
|
|
138
|
-
<div className='mb-1 flex items-center justify-between border-b border-border/40 pb-1'>
|
|
139
|
-
<div className='h-1 w-7 rounded-xs bg-foreground/70' />
|
|
140
|
-
<div className='size-1.5 rounded-full bg-primary' />
|
|
141
|
-
</div>
|
|
142
|
-
<div className='flex flex-1 flex-col justify-between'>
|
|
143
|
-
<div className='h-1 w-full rounded-xs bg-muted' />
|
|
144
|
-
<div className='flex items-center justify-between pt-1'>
|
|
145
|
-
<div className='h-1 w-6 rounded-xs bg-muted' />
|
|
146
|
-
<div className='h-3 w-5 rounded-xs bg-primary shadow-xs' />
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
<div className='mt-auto flex min-h-5 items-center justify-between gap-2 px-0.5'>
|
|
153
|
-
<span
|
|
154
|
-
className={cn(
|
|
155
|
-
'text-xs transition-colors',
|
|
156
|
-
isSelected
|
|
157
|
-
? 'font-medium text-foreground'
|
|
158
|
-
: 'text-muted-foreground group-hover:text-foreground',
|
|
159
|
-
)}
|
|
160
|
-
>
|
|
161
|
-
{t(item.labelKey, {
|
|
162
|
-
defaultValue:
|
|
163
|
-
item.id.charAt(0).toUpperCase() + item.id.slice(1),
|
|
164
|
-
})}
|
|
165
|
-
</span>
|
|
166
|
-
{isSelected && (
|
|
167
|
-
<span
|
|
168
|
-
aria-hidden='true'
|
|
169
|
-
className='flex size-3.5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground'
|
|
170
|
-
>
|
|
171
|
-
<Check className='size-2 stroke-[3]' />
|
|
172
|
-
</span>
|
|
173
|
-
)}
|
|
174
|
-
</div>
|
|
175
|
-
</label>
|
|
176
|
-
);
|
|
177
|
-
})}
|
|
178
|
-
</RadioGroup>
|
|
179
|
-
</fieldset>
|
|
180
|
-
</PopoverContent>
|
|
181
|
-
</Popover>
|
|
64
|
+
</DropdownMenuLabel>
|
|
65
|
+
{themePresets.map((item) => (
|
|
66
|
+
<DropdownMenuRadioItem
|
|
67
|
+
key={item.id}
|
|
68
|
+
value={item.id}
|
|
69
|
+
closeOnClick={false}
|
|
70
|
+
>
|
|
71
|
+
{t(item.labelKey, {
|
|
72
|
+
defaultValue:
|
|
73
|
+
item.id.charAt(0).toUpperCase() + item.id.slice(1),
|
|
74
|
+
})}
|
|
75
|
+
</DropdownMenuRadioItem>
|
|
76
|
+
))}
|
|
77
|
+
</DropdownMenuRadioGroup>
|
|
78
|
+
</DropdownMenuSubContent>
|
|
79
|
+
</DropdownMenuSub>
|
|
182
80
|
);
|
|
183
81
|
}
|
package/config.example.yml
CHANGED
|
@@ -69,30 +69,59 @@ secrets:
|
|
|
69
69
|
- version: 1
|
|
70
70
|
key: replace-with-a-unique-secret
|
|
71
71
|
|
|
72
|
+
# Optional shared sender for Auth, Users invitations and email notification channels. This example is commented out;
|
|
73
|
+
# server/config/mailers.ts defaults to an empty configuration, so example values do not enable mail delivery.
|
|
74
|
+
# `system` is only this template's sample name. Values are literal: ${NAME} is not expanded. The mapping explicitly
|
|
75
|
+
# declares MAILERS_SYSTEM_* variables; inject the password through MAILERS_SYSTEM_PASSWORD.
|
|
76
|
+
# mailers:
|
|
77
|
+
# default: system
|
|
78
|
+
# system:
|
|
79
|
+
# provider: smtp
|
|
80
|
+
# host: smtp.example.com
|
|
81
|
+
# port: 587
|
|
82
|
+
# secure: false
|
|
83
|
+
# auth:
|
|
84
|
+
# user: notifications@example.com
|
|
85
|
+
# from: App <notifications@example.com>
|
|
86
|
+
|
|
87
|
+
# For Resend, use a separate entry with provider: resend, apiKey and from; do not mix provider fields:
|
|
88
|
+
# mailers:
|
|
89
|
+
# default: system
|
|
90
|
+
# system:
|
|
91
|
+
# provider: resend
|
|
92
|
+
# apiKey: replace-with-the-api-key
|
|
93
|
+
# from: "Studio <notifications@example.com>"
|
|
94
|
+
#
|
|
95
|
+
# Equivalent SMTP .env example (use the Resend alternative below instead of its provider-specific SMTP fields):
|
|
96
|
+
# MAILERS_DEFAULT=system
|
|
97
|
+
# MAILERS_SYSTEM_PROVIDER=smtp
|
|
98
|
+
# MAILERS_SYSTEM_HOST=smtp.example.com
|
|
99
|
+
# MAILERS_SYSTEM_PORT=587
|
|
100
|
+
# MAILERS_SYSTEM_SECURE=false
|
|
101
|
+
# MAILERS_SYSTEM_USER=notifications@example.com
|
|
102
|
+
# MAILERS_SYSTEM_PASSWORD=replace-with-the-password
|
|
103
|
+
# MAILERS_SYSTEM_FROM=Studio <notifications@example.com>
|
|
104
|
+
# MAILERS_SYSTEM_REPLY_TO=support@example.com
|
|
105
|
+
# MAILERS_SYSTEM_ENABLED=true
|
|
106
|
+
#
|
|
107
|
+
# Resend .env alternative:
|
|
108
|
+
# MAILERS_DEFAULT=system
|
|
109
|
+
# MAILERS_SYSTEM_PROVIDER=resend
|
|
110
|
+
# MAILERS_SYSTEM_API_KEY=replace-with-the-api-key
|
|
111
|
+
# MAILERS_SYSTEM_FROM=Studio <notifications@example.com>
|
|
112
|
+
# MAILERS_SYSTEM_REPLY_TO=support@example.com
|
|
113
|
+
# MAILERS_SYSTEM_ENABLED=true
|
|
114
|
+
|
|
72
115
|
auth:
|
|
73
|
-
#
|
|
74
|
-
# Set app.publicOrigin (or APP_PUBLIC_ORIGIN) before enabling automatic recovery.
|
|
116
|
+
# Set app.publicOrigin before explicitly enabling automatic password recovery.
|
|
75
117
|
# passwordReset:
|
|
76
118
|
# enabled: true
|
|
77
119
|
# email:
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# port: 587
|
|
81
|
-
# secure: false
|
|
82
|
-
# auth:
|
|
83
|
-
# user: noreply@example.com
|
|
84
|
-
# pass: replace-with-the-smtp-password
|
|
85
|
-
# from: App <noreply@example.com>
|
|
86
|
-
# replyTo: support@example.com
|
|
87
|
-
# For Resend, replace the entire email block with these fields; do not mix SMTP fields:
|
|
88
|
-
# email:
|
|
89
|
-
# provider: resend
|
|
90
|
-
# apiKey: replace-with-the-resend-api-key
|
|
91
|
-
# from: App <noreply@example.com>
|
|
92
|
-
# AUTH_EMAIL_PROVIDER/HOST/PORT/SECURE/USER/PASSWORD/FROM/REPLY_TO/API_KEY override auth.email.
|
|
120
|
+
# mailer: system # omit to use mailers.default
|
|
121
|
+
# Existing inline auth.email and AUTH_EMAIL_* remain supported for compatibility.
|
|
93
122
|
emailAndPassword:
|
|
94
123
|
enabled: true
|
|
95
|
-
autoSignIn:
|
|
124
|
+
autoSignIn: true
|
|
96
125
|
session:
|
|
97
126
|
storeSessionInDatabase: true
|
|
98
127
|
|
|
@@ -102,21 +131,11 @@ notification:
|
|
|
102
131
|
inbox:
|
|
103
132
|
type: in-app
|
|
104
133
|
|
|
105
|
-
#
|
|
106
|
-
# Values are read as written: ${NAME} is not expanded. To take a secret from the environment instead, map a
|
|
107
|
-
# variable onto its path in env of server/config/notification.ts, such as SMTP_PASSWORD.
|
|
134
|
+
# Configuring mailers alone does not create a notification channel.
|
|
108
135
|
# system-email:
|
|
109
|
-
# type:
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# auth:
|
|
113
|
-
# user: notifications@example.com
|
|
114
|
-
# pass: replace-with-the-smtp-password
|
|
115
|
-
# from: notifications@example.com
|
|
116
|
-
# marketing-email:
|
|
117
|
-
# type: resend
|
|
118
|
-
# apiKey: replace-with-the-resend-api-key
|
|
119
|
-
# from: marketing@example.com
|
|
136
|
+
# type: email
|
|
137
|
+
# mailer: system # omit to use mailers.default
|
|
138
|
+
# Legacy type: smtp/resend inline channels and SMTP_PASSWORD/RESEND_API_KEY remain supported.
|
|
120
139
|
|
|
121
140
|
# ops-feishu:
|
|
122
141
|
# type: feishu-webhook
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/app-template-default",
|
|
3
3
|
"displayName": "Default Template",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.88",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"prettier": "@nocobase/dev-config/prettier",
|
|
7
7
|
"engines": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"nocobase": {
|
|
11
11
|
"templateKind": "app",
|
|
12
|
-
"defaultTemplateVersion": "1.0.0-beta.
|
|
12
|
+
"defaultTemplateVersion": "1.0.0-beta.88"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@ai-sdk/react": "4.0.131",
|
|
@@ -74,10 +74,10 @@
|
|
|
74
74
|
"elkjs": "^0.10.2",
|
|
75
75
|
"@types/jsdom": "^30.0.0",
|
|
76
76
|
"@noble/hashes": "2.3.0",
|
|
77
|
-
"@nocobase/app-client": "^4.0.0-beta.
|
|
77
|
+
"@nocobase/app-client": "^4.0.0-beta.8",
|
|
78
78
|
"@nocobase/app-plugin-template-print": "0.0.2-beta.2",
|
|
79
|
-
"@nocobase/app-skills": "0.1.0-beta.
|
|
80
|
-
"@nocobase/app-testing": "0.1.0-beta.
|
|
79
|
+
"@nocobase/app-skills": "0.1.0-beta.59",
|
|
80
|
+
"@nocobase/app-testing": "0.1.0-beta.8",
|
|
81
81
|
"@nocobase/app-websocket": "^0.1.0-beta.1",
|
|
82
82
|
"@nocobase/dev-config": "0.1.0-beta.25",
|
|
83
83
|
"@nocobase/drive": "^0.1.0-beta.7",
|
|
@@ -143,39 +143,39 @@
|
|
|
143
143
|
"better-auth": "1.7.5",
|
|
144
144
|
"hono": "^4.13.2",
|
|
145
145
|
"@nocobase/concurrency": "^0.1.0-beta.1",
|
|
146
|
-
"@nocobase/app-plugin-concurrency": "^0.1.0-beta.
|
|
147
|
-
"@nocobase/app-plugin-agents": "^2.0.0-beta.
|
|
148
|
-
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.
|
|
149
|
-
"@nocobase/app-plugin-authentication": "^2.0.0-beta.
|
|
150
|
-
"@nocobase/app-plugin-authorization": "^1.0.0-beta.
|
|
151
|
-
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.
|
|
152
|
-
"@nocobase/app-plugin-file": "^1.0.0-beta.
|
|
153
|
-
"@nocobase/app-plugin-foundation": "^0.1.0-beta.
|
|
154
|
-
"@nocobase/app-plugin-i18n": "^1.0.0-beta.
|
|
155
|
-
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.
|
|
156
|
-
"@nocobase/app-plugin-mail": "^1.0.0-beta.
|
|
157
|
-
"@nocobase/app-plugin-notification": "^2.0.0-beta.
|
|
158
|
-
"@nocobase/app-plugin-notification-in-app": "^2.0.0-beta.
|
|
159
|
-
"@nocobase/app-plugin-notification-providers": "^1.0.0-beta.
|
|
160
|
-
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.
|
|
161
|
-
"@nocobase/app-plugin-users": "^2.0.0-beta.
|
|
162
|
-
"@nocobase/app-server": "^
|
|
146
|
+
"@nocobase/app-plugin-concurrency": "^0.1.0-beta.2",
|
|
147
|
+
"@nocobase/app-plugin-agents": "^2.0.0-beta.4",
|
|
148
|
+
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.21",
|
|
149
|
+
"@nocobase/app-plugin-authentication": "^2.0.0-beta.19",
|
|
150
|
+
"@nocobase/app-plugin-authorization": "^1.0.0-beta.36",
|
|
151
|
+
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.19",
|
|
152
|
+
"@nocobase/app-plugin-file": "^1.0.0-beta.29",
|
|
153
|
+
"@nocobase/app-plugin-foundation": "^0.1.0-beta.14",
|
|
154
|
+
"@nocobase/app-plugin-i18n": "^1.0.0-beta.21",
|
|
155
|
+
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.17",
|
|
156
|
+
"@nocobase/app-plugin-mail": "^1.0.0-beta.21",
|
|
157
|
+
"@nocobase/app-plugin-notification": "^2.0.0-beta.5",
|
|
158
|
+
"@nocobase/app-plugin-notification-in-app": "^2.0.0-beta.2",
|
|
159
|
+
"@nocobase/app-plugin-notification-providers": "^1.0.0-beta.18",
|
|
160
|
+
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.23",
|
|
161
|
+
"@nocobase/app-plugin-users": "^2.0.0-beta.15",
|
|
162
|
+
"@nocobase/app-server": "^5.0.0-beta.1",
|
|
163
163
|
"@nocobase/config": "^0.1.0-beta.3",
|
|
164
|
-
"@nocobase/db": "^1.0.0-beta.
|
|
164
|
+
"@nocobase/db": "^1.0.0-beta.25",
|
|
165
165
|
"@nocobase/db-sqlite": "^0.1.0-beta.6",
|
|
166
166
|
"@nocobase/service-provider": "^0.0.2-beta.2",
|
|
167
167
|
"@nocobase/snowflake": "^1.0.0-beta.4",
|
|
168
|
-
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.
|
|
169
|
-
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.
|
|
170
|
-
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.
|
|
168
|
+
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.19",
|
|
169
|
+
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.18",
|
|
170
|
+
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.17",
|
|
171
171
|
"@nocobase/authorization": "^1.0.0-beta.16",
|
|
172
172
|
"@nocobase/queue": "^1.0.0-beta.10",
|
|
173
173
|
"@nocobase/jobs": "^1.0.0-beta.5",
|
|
174
174
|
"@nocobase/lifecycle": "^0.1.0-beta.4",
|
|
175
175
|
"@nocobase/i18n": "^2.0.0-beta.1",
|
|
176
|
-
"@nocobase/caching": "^
|
|
176
|
+
"@nocobase/caching": "^1.0.0-beta.4",
|
|
177
177
|
"@nocobase/repository-input": "^0.1.0-beta.3",
|
|
178
|
-
"@nocobase/app-cli": "^1.0.0-beta.
|
|
178
|
+
"@nocobase/app-cli": "^1.0.0-beta.32",
|
|
179
179
|
"@nocobase/realtime": "^1.0.0-beta.0"
|
|
180
180
|
},
|
|
181
181
|
"description": "NocoBase 3 application template with the standard product capabilities.",
|
package/server/config/auth.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { bearer, deviceAuthorization, username } from 'better-auth/plugins';
|
|
|
12
12
|
*/
|
|
13
13
|
const CLI_CLIENT_IDS: readonly string[] = ['nocobase-cli'];
|
|
14
14
|
|
|
15
|
-
// defineAuthConfig
|
|
15
|
+
// Use mailers for shared sending; defineAuthConfig retains AUTH_EMAIL_* for legacy inline senders.
|
|
16
16
|
// Automatic recovery requires passwordReset.enabled: true; leave the switch absent for custom callback compatibility.
|
|
17
17
|
const auth: AppConfigFactory<AuthConfig> = defineAuthConfig({
|
|
18
18
|
defaults: {
|
|
@@ -29,7 +29,7 @@ const auth: AppConfigFactory<AuthConfig> = defineAuthConfig({
|
|
|
29
29
|
}),
|
|
30
30
|
bearer(),
|
|
31
31
|
],
|
|
32
|
-
emailAndPassword: { enabled: true, autoSignIn:
|
|
32
|
+
emailAndPassword: { enabled: true, autoSignIn: true },
|
|
33
33
|
session: { storeSessionInDatabase: true },
|
|
34
34
|
account: { encryptOAuthTokens: true },
|
|
35
35
|
},
|
package/server/config/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import mailers from './mailers.js';
|
|
1
2
|
import {
|
|
2
3
|
defaultAppConfigs,
|
|
3
4
|
type AppConfigFactory,
|
|
@@ -26,6 +27,7 @@ import users from './users.js';
|
|
|
26
27
|
import knowledge from './knowledge.js';
|
|
27
28
|
|
|
28
29
|
const defaultConfigs: AppConfigFactory<{
|
|
30
|
+
mailers: ReturnType<typeof mailers>;
|
|
29
31
|
auth: ReturnType<typeof auth>;
|
|
30
32
|
authorization: ReturnType<typeof authorization>;
|
|
31
33
|
notification: ReturnType<typeof notification>;
|
|
@@ -49,6 +51,7 @@ const defaultConfigs: AppConfigFactory<{
|
|
|
49
51
|
users: ReturnType<typeof users>;
|
|
50
52
|
knowledge: ReturnType<typeof knowledge>;
|
|
51
53
|
}> = defaultAppConfigs({
|
|
54
|
+
mailers,
|
|
52
55
|
auth,
|
|
53
56
|
authorization,
|
|
54
57
|
notification,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineMailersConfig,
|
|
3
|
+
mailerEnvironment,
|
|
4
|
+
} from '@nocobase/app-server/mailers/config';
|
|
5
|
+
|
|
6
|
+
// These mappings are explicit and discoverable through `nocobase config env`.
|
|
7
|
+
// An empty section leaves email unavailable until an operator configures a named service.
|
|
8
|
+
export default defineMailersConfig({
|
|
9
|
+
defaults: {},
|
|
10
|
+
env: { ...mailerEnvironment('system', 'MAILERS_SYSTEM') },
|
|
11
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import NotificationManagementProvider from './notification-management.js';
|
|
1
2
|
import SubmissionProvider from './submission.js';
|
|
2
3
|
import type { ApplicationServiceProviderConstructor } from '@nocobase/app-server/application';
|
|
3
4
|
|
|
4
5
|
const serviceProviders: readonly ApplicationServiceProviderConstructor[] = [
|
|
5
6
|
SubmissionProvider,
|
|
7
|
+
NotificationManagementProvider,
|
|
6
8
|
];
|
|
7
9
|
|
|
8
10
|
export default serviceProviders;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ServiceProvider } from '@nocobase/service-provider';
|
|
2
|
+
import type { AppPluginApplication } from '@nocobase/app-server/plugins';
|
|
3
|
+
import type { MailersConfig } from '@nocobase/app-server/mailers/config';
|
|
4
|
+
import {
|
|
5
|
+
describeInvitationEmailChannel,
|
|
6
|
+
type UsersConfig,
|
|
7
|
+
} from '@nocobase/app-plugin-users/server/config';
|
|
8
|
+
|
|
9
|
+
/** Only a derived boolean reaches the existing SPA bootstrap; all transport configuration stays private. */
|
|
10
|
+
export default class NotificationManagementProvider extends ServiceProvider<AppPluginApplication> {
|
|
11
|
+
readonly name = 'app.notification-management';
|
|
12
|
+
override boot(): Promise<void> {
|
|
13
|
+
const channel = describeInvitationEmailChannel({
|
|
14
|
+
invitations: this.app.config.get<UsersConfig>('users')?.invitations,
|
|
15
|
+
mailers: this.app.config.get<MailersConfig>('mailers'),
|
|
16
|
+
channels: this.app.config.get<{
|
|
17
|
+
channels?: Readonly<Record<string, { enabled?: boolean }>>;
|
|
18
|
+
}>('notification')?.channels,
|
|
19
|
+
});
|
|
20
|
+
this.app.config.mergeDefaults({
|
|
21
|
+
client: {
|
|
22
|
+
notificationTemplates: { invitationConfigured: channel.configured },
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -115,7 +115,7 @@ describe('account permission changes without a browser reload', () => {
|
|
|
115
115
|
await expectMenu(true);
|
|
116
116
|
await changePermissions(view, false);
|
|
117
117
|
invalidate(view);
|
|
118
|
-
expect(screen.
|
|
118
|
+
expect(screen.getByText('users content')).not.toBeVisible();
|
|
119
119
|
await expectMenu(false);
|
|
120
120
|
await changePermissions(view, true);
|
|
121
121
|
invalidate(view);
|
|
@@ -2,7 +2,13 @@ import {
|
|
|
2
2
|
TestI18nProvider,
|
|
3
3
|
createTestI18nRuntime,
|
|
4
4
|
} from '@nocobase/i18n/testing';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
fireEvent,
|
|
7
|
+
render,
|
|
8
|
+
screen,
|
|
9
|
+
waitFor,
|
|
10
|
+
within,
|
|
11
|
+
} from '@testing-library/react';
|
|
6
12
|
import type { ReactElement } from 'react';
|
|
7
13
|
import { MemoryRouter, Route, Routes } from 'react-router';
|
|
8
14
|
import { describe, expect, it, vi } from 'vitest';
|
|
@@ -98,6 +104,9 @@ describe('application authentication pages', () => {
|
|
|
98
104
|
});
|
|
99
105
|
expect(panel).toHaveClass('hidden', 'xl:block');
|
|
100
106
|
expect(panel).toHaveTextContent('AI-native application platform');
|
|
107
|
+
expect(within(panel).getByRole('heading', { level: 2 })).toHaveTextContent(
|
|
108
|
+
/^AI \+ NocoBase\s*Build reliable business systems$/,
|
|
109
|
+
);
|
|
101
110
|
expect(panel).toHaveTextContent('AI-native frontend');
|
|
102
111
|
expect(panel).toHaveTextContent('NocoBase foundation');
|
|
103
112
|
expect(panel).toHaveTextContent('Freedom above. Confidence below.');
|