@fayz-ai/saas 0.6.0 → 0.7.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/README.md +1 -1
- package/dist/app/AdminShell.d.ts +14 -1
- package/dist/app/AdminShell.d.ts.map +1 -1
- package/dist/app/LoginPage.d.ts +2 -16
- package/dist/app/LoginPage.d.ts.map +1 -1
- package/dist/app/admin-app.d.ts +21 -0
- package/dist/app/admin-app.d.ts.map +1 -0
- package/dist/app/config.d.ts +40 -3
- package/dist/app/config.d.ts.map +1 -1
- package/dist/app/scaffold.d.ts +1 -2
- package/dist/app/scaffold.d.ts.map +1 -1
- package/dist/{chunk-DJW67TCY.js → chunk-DCRT47W4.js} +155 -28
- package/dist/chunk-DCRT47W4.js.map +1 -0
- package/dist/{chunk-QGMDVLLW.cjs → chunk-SRIUD35R.cjs} +155 -28
- package/dist/chunk-SRIUD35R.cjs.map +1 -0
- package/dist/crud/CrudDetailPage.d.ts.map +1 -1
- package/dist/hooks/useModuleNavigation.d.ts.map +1 -1
- package/dist/index.cjs +1131 -5792
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +844 -5510
- package/dist/index.js.map +1 -1
- package/dist/org/adapters/supabase.d.ts +18 -0
- package/dist/org/adapters/supabase.d.ts.map +1 -1
- package/dist/org/context.d.ts.map +1 -1
- package/dist/org/index.cjs +11 -10
- package/dist/org/index.js +2 -1
- package/dist/shell/components/crud/archetypes/ClientOrdersTab.d.ts +4 -2
- package/dist/shell/components/crud/archetypes/ClientOrdersTab.d.ts.map +1 -1
- package/dist/shell/components/layout/BottomNav.d.ts.map +1 -1
- package/dist/shell/components/layout/CommandPalette.d.ts.map +1 -1
- package/dist/shell/components/organization/InviteMemberDialog.d.ts.map +1 -1
- package/dist/shell/components/organization/PermissionMatrixEditor.d.ts +6 -1
- package/dist/shell/components/organization/PermissionMatrixEditor.d.ts.map +1 -1
- package/dist/shell/components/organization/PermissionProfilesTab.d.ts.map +1 -1
- package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
- package/dist/shell/components/plugins/PluginSettingsPanel.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedUserProfile.d.ts +2 -1
- package/dist/shell/components/settings/ConnectedUserProfile.d.ts.map +1 -1
- package/dist/shell/components/settings/SettingsPage.d.ts +8 -1
- package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
- package/dist/shell/config/theme/tokens.d.ts +12 -1
- package/dist/shell/config/theme/tokens.d.ts.map +1 -1
- package/dist/shell/config/theme/utils.d.ts +8 -0
- package/dist/shell/config/theme/utils.d.ts.map +1 -1
- package/dist/shell/hooks/useModuleNavigation.d.ts.map +1 -1
- package/dist/shell/lib/i18n.d.ts.map +1 -1
- package/dist/shell/types/client-orders.d.ts +4 -0
- package/dist/shell/types/client-orders.d.ts.map +1 -1
- package/dist/shell/types/crud.d.ts +4 -0
- package/dist/shell/types/crud.d.ts.map +1 -1
- package/package.json +6 -5
- package/src/app/AdminShell.tsx +103 -35
- package/src/app/LoginPage.tsx +2 -264
- package/src/app/{createFayzApp.tsx → admin-app.tsx} +98 -40
- package/src/app/config.ts +44 -2
- package/src/app/scaffold.tsx +13 -26
- package/src/crud/CrudDetailPage.tsx +43 -9
- package/src/crud/CrudPage.tsx +1 -1
- package/src/hooks/useModuleNavigation.ts +7 -1
- package/src/index.ts +12 -5
- package/src/org/adapters/supabase.ts +176 -28
- package/src/org/context.tsx +33 -2
- package/src/shell/components/crud/archetypes/ClientOrdersTab.tsx +12 -4
- package/src/shell/components/layout/BottomNav.tsx +39 -4
- package/src/shell/components/layout/CommandPalette.tsx +14 -10
- package/src/shell/components/organization/InviteMemberDialog.tsx +14 -3
- package/src/shell/components/organization/PermissionMatrixEditor.tsx +48 -10
- package/src/shell/components/organization/PermissionProfilesTab.tsx +65 -23
- package/src/shell/components/organization/TeamTab.tsx +36 -9
- package/src/shell/components/plugins/PluginSettingsPanel.tsx +5 -3
- package/src/shell/components/settings/ConnectedUserProfile.tsx +68 -25
- package/src/shell/components/settings/SettingsPage.tsx +12 -3
- package/src/shell/config/theme/tokens.ts +13 -1
- package/src/shell/config/theme/utils.ts +76 -15
- package/src/shell/hooks/useModuleNavigation.ts +7 -1
- package/src/shell/lib/i18n.ts +4 -0
- package/src/shell/types/client-orders.ts +5 -0
- package/src/shell/types/crud.ts +4 -0
- package/dist/app/createFayzApp.d.ts +0 -11
- package/dist/app/createFayzApp.d.ts.map +0 -1
- package/dist/chunk-DJW67TCY.js.map +0 -1
- package/dist/chunk-QGMDVLLW.cjs.map +0 -1
- package/dist/shell/components/organization/ImpersonationBanner.d.ts +0 -3
- package/dist/shell/components/organization/ImpersonationBanner.d.ts.map +0 -1
- package/dist/shell/components/settings/ConnectedHolidaysSettings.d.ts +0 -3
- package/dist/shell/components/settings/ConnectedHolidaysSettings.d.ts.map +0 -1
- package/dist/shell/components/settings/ConnectedLocationsOverview.d.ts +0 -3
- package/dist/shell/components/settings/ConnectedLocationsOverview.d.ts.map +0 -1
- package/dist/shell/createSaasApp.d.ts +0 -145
- package/dist/shell/createSaasApp.d.ts.map +0 -1
- package/src/shell/components/organization/ImpersonationBanner.tsx +0 -29
- package/src/shell/components/settings/ConnectedHolidaysSettings.tsx +0 -99
- package/src/shell/components/settings/ConnectedLocationsOverview.tsx +0 -57
- package/src/shell/createSaasApp.tsx +0 -943
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import type {
|
|
2
|
+
import type { OrgAdapter, AuthAdapter } from '@fayz-ai/core'
|
|
3
3
|
import {
|
|
4
4
|
PluginRuntimeProvider,
|
|
5
5
|
resolvePluginRuntime,
|
|
@@ -8,7 +8,13 @@ import {
|
|
|
8
8
|
mergeTranslations,
|
|
9
9
|
setCurrentLocale,
|
|
10
10
|
} from '@fayz-ai/core'
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
AuthProvider,
|
|
13
|
+
createAuthRuntime,
|
|
14
|
+
type AuthLayout,
|
|
15
|
+
type AuthPluginOptions,
|
|
16
|
+
type ResolvedAuthPlugin,
|
|
17
|
+
} from '@fayz-ai/plugin-auth'
|
|
12
18
|
import { NavTransitionProvider } from '@fayz-ai/ui'
|
|
13
19
|
import { createFayzSupabaseClient, getFayzSupabaseClientOptional } from '../supabase/client'
|
|
14
20
|
import { OrgProvider } from '../org/context'
|
|
@@ -22,8 +28,7 @@ import { useBillingStore } from '../billing/store'
|
|
|
22
28
|
import type { Plan } from '@fayz-ai/core'
|
|
23
29
|
import type { PlanConfig } from '../shell/types/billing'
|
|
24
30
|
import { useThemeStore } from '../shell/stores/theme.store'
|
|
25
|
-
import { resolveTheme,
|
|
26
|
-
import type { SaasTheme } from '../shell/config/theme/tokens'
|
|
31
|
+
import { resolveTheme, isSaasTheme } from '../shell/config/theme/utils'
|
|
27
32
|
import { AdminShell } from './AdminShell'
|
|
28
33
|
import { useAdminPath } from './routing'
|
|
29
34
|
import type { FayzAppConfig } from './config'
|
|
@@ -32,32 +37,87 @@ import type { FayzAppConfig } from './config'
|
|
|
32
37
|
// Adapter resolution
|
|
33
38
|
// ---------------------------------------------------------------------------
|
|
34
39
|
|
|
35
|
-
function
|
|
40
|
+
function isResolvedAuthPlugin(value: FayzAppConfig['auth']): value is ResolvedAuthPlugin {
|
|
41
|
+
return Boolean(value && 'kind' in value && value.kind === 'auth')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveAuthRuntime(config: FayzAppConfig): ResolvedAuthPlugin {
|
|
36
45
|
const auth = config.auth
|
|
37
|
-
if (auth
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
if (isResolvedAuthPlugin(auth)) return auth
|
|
47
|
+
|
|
48
|
+
const provider = auth?.provider ?? auth?.adapter ?? (config.supabaseUrl ? 'supabase' : 'mock')
|
|
49
|
+
const oauth = auth?.oauth ?? {
|
|
50
|
+
enabled: auth?.showOAuth ?? false,
|
|
51
|
+
providers: auth?.oauthProviders ?? ['google'],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return createAuthRuntime({
|
|
55
|
+
provider,
|
|
56
|
+
requireAuth: auth?.requireAuth ?? true,
|
|
57
|
+
layout: auth?.layout ?? auth?.loginLayout,
|
|
58
|
+
logo: auth?.logo ?? auth?.loginLogo,
|
|
59
|
+
tagline: auth?.tagline ?? auth?.loginTagline,
|
|
60
|
+
description: auth?.description ?? auth?.loginDescription,
|
|
61
|
+
oauth,
|
|
62
|
+
routes: auth?.routes,
|
|
63
|
+
supabase: {
|
|
64
|
+
url: auth?.supabase?.url ?? config.supabaseUrl,
|
|
65
|
+
anonKey: auth?.supabase?.anonKey ?? config.supabaseAnonKey,
|
|
66
|
+
client: auth?.supabase?.client ?? getFayzSupabaseClientOptional() ?? undefined,
|
|
67
|
+
callbackUrl: auth?.supabase?.callbackUrl,
|
|
68
|
+
resetPasswordUrl: auth?.supabase?.resetPasswordUrl,
|
|
69
|
+
},
|
|
70
|
+
} satisfies AuthPluginOptions)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getAuthShellProps(auth: FayzAppConfig['auth']): {
|
|
74
|
+
requireAuth?: boolean
|
|
75
|
+
loginTagline?: string
|
|
76
|
+
loginDescription?: string
|
|
77
|
+
loginLogo?: React.ReactNode
|
|
78
|
+
loginLayout?: AuthLayout
|
|
79
|
+
showOAuth?: boolean
|
|
80
|
+
oauthProviders?: Exclude<import('@fayz-ai/core').AuthProvider, 'email'>[]
|
|
81
|
+
} {
|
|
82
|
+
if (!auth) return {}
|
|
83
|
+
if (isResolvedAuthPlugin(auth)) {
|
|
84
|
+
return {
|
|
85
|
+
requireAuth: auth.requireAuth,
|
|
86
|
+
loginTagline: auth.tagline,
|
|
87
|
+
loginDescription: auth.description,
|
|
88
|
+
loginLogo: auth.logo,
|
|
89
|
+
loginLayout: auth.layout,
|
|
90
|
+
showOAuth: auth.oauth.enabled,
|
|
91
|
+
oauthProviders: auth.oauth.providers,
|
|
45
92
|
}
|
|
46
|
-
return createSupabaseAuthAdapter({
|
|
47
|
-
supabaseUrl: config.supabaseUrl,
|
|
48
|
-
supabaseAnonKey: config.supabaseAnonKey,
|
|
49
|
-
supabaseClient: getFayzSupabaseClientOptional() ?? undefined,
|
|
50
|
-
})
|
|
51
93
|
}
|
|
52
|
-
return
|
|
94
|
+
return {
|
|
95
|
+
requireAuth: auth.requireAuth,
|
|
96
|
+
loginTagline: auth.tagline ?? auth.loginTagline,
|
|
97
|
+
loginDescription: auth.description ?? auth.loginDescription,
|
|
98
|
+
loginLogo: auth.logo ?? auth.loginLogo,
|
|
99
|
+
loginLayout: auth.layout ?? auth.loginLayout,
|
|
100
|
+
showOAuth: auth.oauth?.enabled ?? auth.showOAuth,
|
|
101
|
+
oauthProviders: auth.oauth?.providers ?? auth.oauthProviders,
|
|
102
|
+
}
|
|
53
103
|
}
|
|
54
104
|
|
|
55
|
-
function resolveOrgAdapter(config: FayzAppConfig): OrgAdapter {
|
|
105
|
+
function resolveOrgAdapter(config: FayzAppConfig, authAdapter?: AuthAdapter): OrgAdapter {
|
|
56
106
|
const org = config.org
|
|
57
107
|
if (org?.adapter && typeof org.adapter === 'object') return org.adapter as OrgAdapter
|
|
58
108
|
|
|
59
109
|
const strategy = org?.adapter ?? (config.supabaseUrl ? 'supabase' : 'mock')
|
|
60
|
-
|
|
110
|
+
// The app declares its own business roles via permissions.defaultProfiles; pass
|
|
111
|
+
// their identity (id/name/description) to both adapters so the role SET is
|
|
112
|
+
// app-owned, not baked into the SDK.
|
|
113
|
+
const declaredRoles = config.permissions?.defaultProfiles?.map((p) => ({
|
|
114
|
+
id: p.id,
|
|
115
|
+
name: p.name,
|
|
116
|
+
description: p.description,
|
|
117
|
+
}))
|
|
118
|
+
// Inject the resolved auth adapter so the org adapter can DELIVER invites (send
|
|
119
|
+
// the e-mail + create the auth user) — org owns the audit row + role, auth owns delivery.
|
|
120
|
+
if (strategy === 'supabase') return createSupabaseOrgAdapter({ roles: declaredRoles, authAdapter })
|
|
61
121
|
return createMockOrgAdapter(config.permissions?.defaultProfiles)
|
|
62
122
|
}
|
|
63
123
|
|
|
@@ -116,12 +176,9 @@ function ThemeInitializer({ config }: { config: FayzAppConfig }) {
|
|
|
116
176
|
|
|
117
177
|
React.useEffect(() => {
|
|
118
178
|
if (config.theme) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const resolved = isSaasTheme
|
|
123
|
-
? resolveTheme(config.theme as SaasTheme)
|
|
124
|
-
: config.theme as CreateThemeOptions
|
|
179
|
+
const resolved = isSaasTheme(config.theme)
|
|
180
|
+
? resolveTheme(config.theme)
|
|
181
|
+
: config.theme
|
|
125
182
|
setOverrides(resolved)
|
|
126
183
|
}
|
|
127
184
|
if (config.defaultThemeMode && typeof window !== 'undefined' && !localStorage.getItem('saas-core:theme-mode')) {
|
|
@@ -134,7 +191,7 @@ function ThemeInitializer({ config }: { config: FayzAppConfig }) {
|
|
|
134
191
|
}
|
|
135
192
|
|
|
136
193
|
// ---------------------------------------------------------------------------
|
|
137
|
-
// AdminProviders — the provider stack shared by the
|
|
194
|
+
// AdminProviders — the provider stack shared by the admin factory/createSaasApp
|
|
138
195
|
// sugar path AND the manifest-driven AdminScaffold. Resolves adapters + i18n
|
|
139
196
|
// once (stable refs) and recomputes the plugin runtime reactively as the path
|
|
140
197
|
// changes, so route-scoped widget visibility stays correct.
|
|
@@ -152,16 +209,20 @@ export function AdminProviders({ config, children }: { config: FayzAppConfig; ch
|
|
|
152
209
|
createFayzSupabaseClient(config.supabaseUrl, config.supabaseAnonKey)
|
|
153
210
|
}
|
|
154
211
|
const i18n = buildI18nConfig(config)
|
|
155
|
-
|
|
212
|
+
const authRuntime = resolveAuthRuntime(config)
|
|
156
213
|
return {
|
|
157
|
-
|
|
158
|
-
orgAdapter: resolveOrgAdapter(config),
|
|
214
|
+
authRuntime,
|
|
215
|
+
orgAdapter: resolveOrgAdapter(config, authRuntime.adapter),
|
|
159
216
|
i18n,
|
|
160
217
|
plugins: config.plugins ?? [],
|
|
161
218
|
}
|
|
162
219
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
220
|
}, [config])
|
|
164
221
|
|
|
222
|
+
React.useEffect(() => {
|
|
223
|
+
setCurrentLocale(resolved.i18n.defaultLocale)
|
|
224
|
+
}, [resolved.i18n.defaultLocale])
|
|
225
|
+
|
|
165
226
|
// Recompute the plugin runtime when the hash path or tenant changes
|
|
166
227
|
// (route-scoped visibility + tenant-scoped plugin data providers).
|
|
167
228
|
const path = useAdminPath()
|
|
@@ -188,7 +249,7 @@ export function AdminProviders({ config, children }: { config: FayzAppConfig; ch
|
|
|
188
249
|
)
|
|
189
250
|
|
|
190
251
|
return (
|
|
191
|
-
<AuthProvider adapter={resolved.
|
|
252
|
+
<AuthProvider adapter={resolved.authRuntime.adapter}>
|
|
192
253
|
<OrgProvider adapter={resolved.orgAdapter} autoCreate={config.org?.autoCreate ?? true}>
|
|
193
254
|
<PluginRuntimeProvider value={runtime}>
|
|
194
255
|
<PermissionsProvider config={config.permissions}>
|
|
@@ -224,6 +285,7 @@ AdminProviders.displayName = 'AdminProviders'
|
|
|
224
285
|
// ---------------------------------------------------------------------------
|
|
225
286
|
|
|
226
287
|
export function createFayzApp(config: FayzAppConfig): React.ComponentType {
|
|
288
|
+
const authShellProps = getAuthShellProps(config.auth)
|
|
227
289
|
function FayzApp({ children }: { children?: React.ReactNode }) {
|
|
228
290
|
return (
|
|
229
291
|
<AdminProviders config={config}>
|
|
@@ -234,16 +296,12 @@ export function createFayzApp(config: FayzAppConfig): React.ComponentType {
|
|
|
234
296
|
layout={config.layout}
|
|
235
297
|
contentFrame={config.contentFrame}
|
|
236
298
|
moduleNav={config.moduleNav}
|
|
299
|
+
bottomNav={config.bottomNav}
|
|
237
300
|
pages={config.pages}
|
|
238
|
-
|
|
239
|
-
loginTagline={config.auth?.loginTagline}
|
|
240
|
-
loginDescription={config.auth?.loginDescription}
|
|
241
|
-
loginLogo={config.auth?.loginLogo}
|
|
242
|
-
loginLayout={config.auth?.loginLayout}
|
|
243
|
-
showOAuth={config.auth?.showOAuth}
|
|
244
|
-
oauthProviders={config.auth?.oauthProviders}
|
|
301
|
+
{...authShellProps}
|
|
245
302
|
showSettings
|
|
246
|
-
showOrgSettings={Boolean(config.org)}
|
|
303
|
+
showOrgSettings={config.orgSettings ?? Boolean(config.org)}
|
|
304
|
+
showBranding={config.branding ?? true}
|
|
247
305
|
/>
|
|
248
306
|
)}
|
|
249
307
|
</AdminProviders>
|
package/src/app/config.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
LocaleConfig,
|
|
9
9
|
PermissionsConfig,
|
|
10
10
|
} from '@fayz-ai/core'
|
|
11
|
+
import type { AuthPluginOptions, ResolvedAuthPlugin } from '@fayz-ai/plugin-auth'
|
|
12
|
+
import type { BottomNavItem, MobileHeaderVariant } from '@fayz-ai/ui'
|
|
11
13
|
import type { SaasTheme } from '../shell/config/theme/tokens'
|
|
12
14
|
import type { CreateThemeOptions } from '../shell/config/theme/utils'
|
|
13
15
|
import type { PlanConfig } from '../shell/types/billing'
|
|
@@ -30,6 +32,11 @@ export interface CustomPage {
|
|
|
30
32
|
position?: number
|
|
31
33
|
badge?: string | number
|
|
32
34
|
permission?: { feature: string; action: 'read' | 'create' | 'edit' | 'delete' }
|
|
35
|
+
/** Show this page in the sidebar/topbar navigation. Default: true. Set `false`
|
|
36
|
+
* for mobile-only pages reachable via bottomNav/avatar (e.g. a "Mais" overflow
|
|
37
|
+
* hub or a "Perfil" avatar target): the route still works, but the entry is
|
|
38
|
+
* hidden from desktop navigation where those mobile constructs are meaningless. */
|
|
39
|
+
nav?: boolean
|
|
33
40
|
children?: CustomPage[]
|
|
34
41
|
}
|
|
35
42
|
|
|
@@ -39,9 +46,19 @@ export interface CustomPage {
|
|
|
39
46
|
|
|
40
47
|
export interface AuthConfig {
|
|
41
48
|
/** 'supabase' | 'mock' or bring your own AuthAdapter */
|
|
42
|
-
adapter?: '
|
|
49
|
+
adapter?: AuthPluginOptions['adapter']
|
|
50
|
+
/** New plugin-auth provider selector. Existing apps may keep using adapter. */
|
|
51
|
+
provider?: AuthPluginOptions['provider']
|
|
43
52
|
/** Redirect to login when no session is found (default: true) */
|
|
44
53
|
requireAuth?: boolean
|
|
54
|
+
/** New plugin-auth route and provider options. */
|
|
55
|
+
routes?: AuthPluginOptions['routes']
|
|
56
|
+
supabase?: AuthPluginOptions['supabase']
|
|
57
|
+
oauth?: AuthPluginOptions['oauth']
|
|
58
|
+
layout?: AuthPluginOptions['layout']
|
|
59
|
+
logo?: ReactNode
|
|
60
|
+
tagline?: string
|
|
61
|
+
description?: string
|
|
45
62
|
loginLogo?: ReactNode
|
|
46
63
|
loginLayout?: 'split' | 'centered'
|
|
47
64
|
loginTagline?: string
|
|
@@ -50,6 +67,8 @@ export interface AuthConfig {
|
|
|
50
67
|
oauthProviders?: Exclude<AuthProvider, 'email'>[]
|
|
51
68
|
}
|
|
52
69
|
|
|
70
|
+
export type AppAuthConfig = AuthConfig | ResolvedAuthPlugin
|
|
71
|
+
|
|
53
72
|
export interface OrgConfig {
|
|
54
73
|
/** 'supabase' | 'mock' or bring your own OrgAdapter */
|
|
55
74
|
adapter?: 'supabase' | 'mock' | OrgAdapter
|
|
@@ -90,7 +109,7 @@ export interface FayzAppConfig {
|
|
|
90
109
|
// -------------------------------------------------------------------------
|
|
91
110
|
// Auth
|
|
92
111
|
// -------------------------------------------------------------------------
|
|
93
|
-
auth?:
|
|
112
|
+
auth?: AppAuthConfig
|
|
94
113
|
|
|
95
114
|
// -------------------------------------------------------------------------
|
|
96
115
|
// Organization / multi-tenancy
|
|
@@ -114,6 +133,17 @@ export interface FayzAppConfig {
|
|
|
114
133
|
defaultThemeMode?: ThemeMode
|
|
115
134
|
/** Shell layout variant (default: 'sidebar') */
|
|
116
135
|
layout?: 'sidebar' | 'topbar' | 'minimal'
|
|
136
|
+
/** Mobile bottom tab bar. Rendered by AppShell on small screens (md:hidden)
|
|
137
|
+
* for every layout variant. Route items map a lucide icon name to a route; an
|
|
138
|
+
* `{ kind: 'action', id, icon }` item renders as a raised center button
|
|
139
|
+
* (Mobills style) that fires `onBottomNavAction(id)` instead of navigating. */
|
|
140
|
+
bottomNav?: BottomNavItem[]
|
|
141
|
+
/** Fired when a bottom-nav `action` item (e.g. the center "+") is tapped. */
|
|
142
|
+
onBottomNavAction?: (id: string) => void
|
|
143
|
+
/** Mobile header treatment (<md), default 'minimal'. 'transparent' renders no
|
|
144
|
+
* header bar (edge-to-edge content) with a floating profile avatar top-right;
|
|
145
|
+
* 'hidden' renders neither. Desktop keeps its sidebar + user menu. */
|
|
146
|
+
mobileHeader?: MobileHeaderVariant
|
|
117
147
|
/** Wrap the main content in an inset "framed" card (default: true). The
|
|
118
148
|
* sidebar itself is always flush/full-height. */
|
|
119
149
|
contentFrame?: boolean
|
|
@@ -129,6 +159,18 @@ export interface FayzAppConfig {
|
|
|
129
159
|
// -------------------------------------------------------------------------
|
|
130
160
|
locale?: LocaleConfig
|
|
131
161
|
|
|
162
|
+
// -------------------------------------------------------------------------
|
|
163
|
+
// Settings surface
|
|
164
|
+
// -------------------------------------------------------------------------
|
|
165
|
+
/** Show org-level settings tabs (Equipe/Permissões/Localizações/Regras).
|
|
166
|
+
* Defaults to `Boolean(org)` for back-compat. B2C/personal apps that still
|
|
167
|
+
* need an org for the workspace can set `false` to hide these ERP tabs. */
|
|
168
|
+
orgSettings?: boolean
|
|
169
|
+
/** Show the branding ("Identidade Visual") + company "Geral" settings tabs.
|
|
170
|
+
* Default: `true`. B2C apps set `false` to drop org-identity settings, leaving
|
|
171
|
+
* Perfil + Segurança + plugin settings only. */
|
|
172
|
+
branding?: boolean
|
|
173
|
+
|
|
132
174
|
// -------------------------------------------------------------------------
|
|
133
175
|
// Permissions
|
|
134
176
|
// -------------------------------------------------------------------------
|
package/src/app/scaffold.tsx
CHANGED
|
@@ -12,10 +12,9 @@ import type {
|
|
|
12
12
|
PermissionsConfig,
|
|
13
13
|
BillingConfig as ManifestBillingConfig,
|
|
14
14
|
} from '@fayz-ai/core'
|
|
15
|
-
import { AdminProviders } from './
|
|
15
|
+
import { AdminProviders, getAuthShellProps } from './admin-app'
|
|
16
16
|
import { AdminShell } from './AdminShell'
|
|
17
17
|
import type { FayzAppConfig, CustomPage } from './config'
|
|
18
|
-
import { createSaasApp, type SaasAppConfig } from '../shell/createSaasApp'
|
|
19
18
|
|
|
20
19
|
// ---------------------------------------------------------------------------
|
|
21
20
|
// Admin scaffold — renders an admin app from a pure-data AppManifest. Mirrors
|
|
@@ -32,13 +31,9 @@ function slug(name: string): string {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
const liveConfigOption = '__fayzLiveConfigRef'
|
|
35
|
-
const liveSaasConfigs = new Map<string, FayzAppConfig
|
|
34
|
+
const liveSaasConfigs = new Map<string, FayzAppConfig>()
|
|
36
35
|
|
|
37
|
-
function
|
|
38
|
-
return 'organization' in config || 'settingsTabs' in config || 'bottomNav' in config || 'pluginRuntime' in config
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getAuthRequireAuth(config: FayzAppConfig | SaasAppConfig): boolean {
|
|
36
|
+
function getAuthRequireAuth(config: FayzAppConfig): boolean {
|
|
42
37
|
return config.auth?.requireAuth ?? true
|
|
43
38
|
}
|
|
44
39
|
|
|
@@ -50,7 +45,7 @@ function getLiveConfigRef(id: string): string {
|
|
|
50
45
|
* migration, config-authored apps keep a live config ref so renderApp can
|
|
51
46
|
* preserve code-backed plugins and custom pages while the JSON manifest path
|
|
52
47
|
* matures. Serialized manifests still resolve through plugin/page registries. */
|
|
53
|
-
export function defineSaas(config: FayzAppConfig
|
|
48
|
+
export function defineSaas(config: FayzAppConfig): AppManifest {
|
|
54
49
|
const id = slug(config.name)
|
|
55
50
|
const configRef = getLiveConfigRef(id)
|
|
56
51
|
liveSaasConfigs.set(configRef, config)
|
|
@@ -121,7 +116,7 @@ function manifestToFayzConfig(manifest: AppManifest, surfaceName: string): FayzA
|
|
|
121
116
|
const options = (manifest.surfaces[surfaceName]?.options ?? {}) as Record<string, unknown>
|
|
122
117
|
const liveConfigRef = options[liveConfigOption]
|
|
123
118
|
const liveConfig = typeof liveConfigRef === 'string' ? liveSaasConfigs.get(liveConfigRef) : undefined
|
|
124
|
-
if (liveConfig
|
|
119
|
+
if (liveConfig) return liveConfig
|
|
125
120
|
|
|
126
121
|
return {
|
|
127
122
|
name: manifest.name,
|
|
@@ -138,15 +133,8 @@ function manifestToFayzConfig(manifest: AppManifest, surfaceName: string): FayzA
|
|
|
138
133
|
}
|
|
139
134
|
|
|
140
135
|
export function AdminScaffold({ manifest, surface }: { manifest: AppManifest; surface: string }) {
|
|
141
|
-
const liveConfigRef = manifest.surfaces[surface]?.options?.[liveConfigOption]
|
|
142
|
-
const liveConfig = typeof liveConfigRef === 'string' ? liveSaasConfigs.get(liveConfigRef) : undefined
|
|
143
|
-
const LegacyApp = React.useMemo(() => {
|
|
144
|
-
if (!liveConfig || !hasLegacyShellConfig(liveConfig)) return null
|
|
145
|
-
return createSaasApp(liveConfig)
|
|
146
|
-
}, [liveConfig])
|
|
147
136
|
const config = React.useMemo(() => manifestToFayzConfig(manifest, surface), [manifest, surface])
|
|
148
|
-
|
|
149
|
-
if (LegacyApp) return <LegacyApp />
|
|
137
|
+
const authShellProps = React.useMemo(() => getAuthShellProps(config.auth), [config.auth])
|
|
150
138
|
|
|
151
139
|
return (
|
|
152
140
|
<AdminProviders config={config}>
|
|
@@ -154,16 +142,15 @@ export function AdminScaffold({ manifest, surface }: { manifest: AppManifest; su
|
|
|
154
142
|
appName={config.name}
|
|
155
143
|
logo={config.logo}
|
|
156
144
|
layout={config.layout}
|
|
145
|
+
bottomNav={config.bottomNav}
|
|
146
|
+
onBottomNavAction={config.onBottomNavAction}
|
|
147
|
+
mobileHeader={config.mobileHeader}
|
|
157
148
|
pages={config.pages}
|
|
158
|
-
|
|
159
|
-
loginTagline={config.auth?.loginTagline}
|
|
160
|
-
loginDescription={config.auth?.loginDescription}
|
|
161
|
-
loginLogo={config.auth?.loginLogo}
|
|
162
|
-
loginLayout={config.auth?.loginLayout}
|
|
163
|
-
showOAuth={config.auth?.showOAuth}
|
|
164
|
-
oauthProviders={config.auth?.oauthProviders}
|
|
149
|
+
{...authShellProps}
|
|
165
150
|
showSettings
|
|
166
|
-
showOrgSettings={Boolean(config.org)}
|
|
151
|
+
showOrgSettings={config.orgSettings ?? Boolean(config.org)}
|
|
152
|
+
showOrgSwitcher={config.org?.multiOrg !== false}
|
|
153
|
+
showBranding={config.branding ?? true}
|
|
167
154
|
/>
|
|
168
155
|
</AdminProviders>
|
|
169
156
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useCallback } from 'react'
|
|
2
|
-
import { Edit, Trash2, Eye, Shield, CalendarDays, FileText, Clock, Package, Users, DollarSign, MapPin, BarChart3, Briefcase, ShoppingBag } from 'lucide-react'
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
import { Edit, Trash2, Eye, Shield, CalendarDays, FileText, Clock, Package, Users, DollarSign, MapPin, BarChart3, Briefcase, ShoppingBag, FolderOpen, MessageSquare, Route } from 'lucide-react'
|
|
3
3
|
import type { LucideIcon } from 'lucide-react'
|
|
4
4
|
import { Card, CardContent } from '@fayz-ai/ui'
|
|
5
5
|
import { Button } from '@fayz-ai/ui'
|
|
@@ -223,8 +223,35 @@ function getArchetypeTabs(layout?: FormLayout, archetypeKind?: string): DetailTa
|
|
|
223
223
|
return tabs
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
function mergeDetailTabs(archetypeTabs: DetailTab[], entityTabs: DetailTab[]): DetailTab[] {
|
|
227
|
+
const merged = new Map<string, DetailTab>()
|
|
228
|
+
|
|
229
|
+
for (const tab of entityTabs) merged.set(tab.id, tab)
|
|
230
|
+
for (const tab of archetypeTabs) {
|
|
231
|
+
if (!merged.has(tab.id)) merged.set(tab.id, tab)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return Array.from(merged.values())
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeTabId(value?: string): string | undefined {
|
|
238
|
+
return value?.split('?')[0]
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveActiveTab(initialTab: string | undefined, tabs: DetailTab[]): string {
|
|
242
|
+
const normalized = normalizeTabId(initialTab)
|
|
243
|
+
if (!normalized) return 'overview'
|
|
244
|
+
if (normalized === 'overview') return 'overview'
|
|
245
|
+
|
|
246
|
+
const direct = tabs.find((tab) => tab.id === normalized)
|
|
247
|
+
if (direct) return direct.id
|
|
248
|
+
|
|
249
|
+
const alias = tabs.find((tab) => (tab as DetailTab & { aliases?: string[] }).aliases?.includes(normalized))
|
|
250
|
+
return alias?.id ?? 'overview'
|
|
251
|
+
}
|
|
252
|
+
|
|
226
253
|
const TAB_ICONS: Record<string, LucideIcon> = {
|
|
227
|
-
Shield, CalendarDays, FileText, Clock, Package, Users, DollarSign, MapPin, BarChart3, Briefcase, ShoppingBag,
|
|
254
|
+
Shield, CalendarDays, FileText, Clock, Package, Users, DollarSign, MapPin, BarChart3, Briefcase, ShoppingBag, FolderOpen, MessageSquare, Route,
|
|
228
255
|
}
|
|
229
256
|
|
|
230
257
|
export function CrudDetailPage({
|
|
@@ -246,21 +273,28 @@ export function CrudDetailPage({
|
|
|
246
273
|
const imageValue = entityDef.imageField ? item[entityDef.imageField] : null
|
|
247
274
|
const initial = typeof displayValue === 'string' ? displayValue.charAt(0).toUpperCase() : '?'
|
|
248
275
|
|
|
249
|
-
// Merge archetype tabs + custom entity tabs
|
|
276
|
+
// Merge archetype tabs + custom entity tabs. Entity-specific tabs win by id
|
|
277
|
+
// so vertical apps can replace generic person/product tabs without duplicates.
|
|
250
278
|
const runtime = usePluginRuntimeOptional()
|
|
251
279
|
const archetypeTabs = getArchetypeTabs(entityDef.layout, entityDef.data?.archetypeKind)
|
|
252
|
-
const customTabs =
|
|
280
|
+
const customTabs = mergeDetailTabs(archetypeTabs, entityDef.detailTabs ?? [])
|
|
253
281
|
// Drop tabs that require a plugin-contributed widget when none is registered
|
|
254
282
|
// (e.g. the financial statement tab only appears when the financial plugin is active).
|
|
255
283
|
.filter((tab) => !tab.requiresWidgetZone || (runtime ? getWidgetsForZone(runtime, tab.requiresWidgetZone).length > 0 : false))
|
|
284
|
+
.filter((tab) => !(tab as DetailTab & { hidden?: boolean }).hidden)
|
|
256
285
|
const entityId = entityDef.name.toLowerCase().replace(/\s+/g, '-')
|
|
257
286
|
const widgetZone = `${entityId}.detail.tabs`
|
|
258
287
|
|
|
259
|
-
// Resolve initial tab —
|
|
260
|
-
const
|
|
261
|
-
const
|
|
288
|
+
// Resolve initial tab — allow compatibility aliases, fallback to "overview".
|
|
289
|
+
const activeTab = resolveActiveTab(initialTab, customTabs)
|
|
290
|
+
const [selectedTab, setSelectedTab] = useState(activeTab)
|
|
291
|
+
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
setSelectedTab(activeTab)
|
|
294
|
+
}, [activeTab])
|
|
262
295
|
|
|
263
296
|
const handleTabChange = useCallback((value: string) => {
|
|
297
|
+
setSelectedTab(value)
|
|
264
298
|
if (embedded) return
|
|
265
299
|
const detailPath = `${basePath}/${item.id}`
|
|
266
300
|
const newHash = value === 'overview' ? detailPath : `${detailPath}/${value}`
|
|
@@ -326,7 +360,7 @@ export function CrudDetailPage({
|
|
|
326
360
|
{!embedded && <Separator />}
|
|
327
361
|
|
|
328
362
|
{/* Tabs */}
|
|
329
|
-
<Tabs
|
|
363
|
+
<Tabs value={selectedTab} onValueChange={handleTabChange}>
|
|
330
364
|
<TabsList className={`max-w-full justify-start overflow-x-auto scrollbar-none${embedded ? ' h-8' : ''}`}>
|
|
331
365
|
<TabsTrigger value="overview" className={`gap-1.5 ${embedded ? 'text-xs px-2 py-1' : ''}`}>
|
|
332
366
|
<Eye className={embedded ? 'h-3 w-3' : 'h-3.5 w-3.5'} />
|
package/src/crud/CrudPage.tsx
CHANGED
|
@@ -349,7 +349,7 @@ export function CrudPage<T extends { id: string }>({ entityDef: rawEntityDef, us
|
|
|
349
349
|
// Parse /uuid or /uuid/tab-name
|
|
350
350
|
const subParts = sub.slice(1).split('/')
|
|
351
351
|
const id = subParts[0]
|
|
352
|
-
const initialTab = subParts[1] || undefined
|
|
352
|
+
const initialTab = subParts[1]?.split('?')[0] || undefined
|
|
353
353
|
const item = store.getById(id)
|
|
354
354
|
viewKey = `detail-${id}`
|
|
355
355
|
|
|
@@ -44,13 +44,19 @@ export function useModuleNavigation(hashBase: string, depthMap?: DepthMap, homeV
|
|
|
44
44
|
// '/inventory/dashboard' → treat as home view
|
|
45
45
|
if (segments.length === 1 && segments[0] === homeView) return homeView
|
|
46
46
|
// Detect action + ID patterns: '/products/edit/uuid' or '/stock/detail/uuid'
|
|
47
|
-
const ACTION_KEYWORDS = ['edit', 'detail', 'view']
|
|
47
|
+
const ACTION_KEYWORDS = ['edit', 'detail', 'view', 'post']
|
|
48
48
|
if (segments.length >= 3) {
|
|
49
49
|
const maybeAction = segments[segments.length - 2]
|
|
50
50
|
if (ACTION_KEYWORDS.includes(maybeAction)) {
|
|
51
51
|
return segments.slice(0, -2).join('-') + '-' + maybeAction + ':' + segments[segments.length - 1]
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
// Generic fallback: a trailing UUID is always an ID for the view formed
|
|
55
|
+
// by the preceding segments ('/content/post/<uuid>' → 'content-post:<uuid>')
|
|
56
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
57
|
+
if (segments.length >= 2 && UUID_RE.test(segments[segments.length - 1])) {
|
|
58
|
+
return segments.slice(0, -1).join('-') + ':' + segments[segments.length - 1]
|
|
59
|
+
}
|
|
54
60
|
return segments.join('-')
|
|
55
61
|
}
|
|
56
62
|
return homeView
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// App factory (main entry point)
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
export { createFayzApp, AdminProviders } from './app/
|
|
4
|
+
export { createFayzApp, AdminProviders } from './app/admin-app'
|
|
5
5
|
export type { FayzAppConfig, AuthConfig, OrgConfig, ChatConfig, CustomPage, PageSection } from './app/config'
|
|
6
6
|
|
|
7
7
|
// Native admin scaffold — manifest-first entry. Importing this registers the
|
|
@@ -11,6 +11,11 @@ export { AdminShell } from './app/AdminShell'
|
|
|
11
11
|
export { LoginPage } from './app/LoginPage'
|
|
12
12
|
export { navigateTo as adminNavigateTo, useAdminPath } from './app/routing'
|
|
13
13
|
|
|
14
|
+
// Auth hook re-exported through the saas front-door so app screens (e.g. a
|
|
15
|
+
// Profile page) can read the current user + signOut without depending on
|
|
16
|
+
// @fayz-ai/auth directly.
|
|
17
|
+
export { useAuth } from '@fayz-ai/auth'
|
|
18
|
+
|
|
14
19
|
// ---------------------------------------------------------------------------
|
|
15
20
|
// CRUD engine — NATIVE code is now in ./crud (de-bridged from saas-core: the
|
|
16
21
|
// full list/form/detail engine, archetype layouts, providers and store).
|
|
@@ -34,10 +39,6 @@ export { PermissionGate } from './permissions/PermissionGate'
|
|
|
34
39
|
export { WidgetSlot } from './plugins/WidgetSlot'
|
|
35
40
|
export { useFieldRules } from './hooks/useFieldRules'
|
|
36
41
|
|
|
37
|
-
// Native admin shell — createSaasApp orchestrator (de-bridged from saas-core
|
|
38
|
-
// into ./shell). The default createCrudPage is the native one.
|
|
39
|
-
export { createSaasApp } from './shell/createSaasApp'
|
|
40
|
-
export type { SaasAppConfig, PageConfig } from './shell/createSaasApp'
|
|
41
42
|
export { createCrudPage } from './crud/createCrudPage'
|
|
42
43
|
// Client-orders archetype (used by beauty-saas client detail tab)
|
|
43
44
|
export { ClientOrdersTab } from './shell/components/crud/archetypes/ClientOrdersTab'
|
|
@@ -173,6 +174,12 @@ export { resolveEntityHref } from './lib/entity-routes'
|
|
|
173
174
|
export { usePluginPrefs } from './hooks/usePluginPrefs'
|
|
174
175
|
export { setScheduleBlockConfig, getScheduleBlockConfig, subscribeScheduleBlockConfig } from './lib/schedule-config'
|
|
175
176
|
|
|
177
|
+
// Assistant chat store — lets apps drive the shell's chat panel imperatively
|
|
178
|
+
// (e.g. an "Ask the assistant" CTA calling useChatStore().setOpen(true)). The
|
|
179
|
+
// store is a shared zustand singleton, so app + shell read the same instance.
|
|
180
|
+
export { useChatStore } from './shell/stores/chat.store'
|
|
181
|
+
export type { ChatMessage } from './shell/stores/chat.store'
|
|
182
|
+
|
|
176
183
|
// Shell plugin-settings panel + dedup util (used by vertical plugins)
|
|
177
184
|
export { PluginSettingsPanel } from './shell/components/plugins/PluginSettingsPanel'
|
|
178
185
|
export { ConnectorsHub } from './shell/components/plugins/ConnectorsHub'
|