@fayz-ai/saas 0.8.0 → 0.8.2
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/dist/access/context.d.ts +47 -0
- package/dist/access/context.d.ts.map +1 -0
- package/dist/access/headless.d.ts +21 -0
- package/dist/access/headless.d.ts.map +1 -0
- package/dist/access/index.d.ts +12 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/limits-registry.d.ts +33 -0
- package/dist/access/limits-registry.d.ts.map +1 -0
- package/dist/access/module-nav.d.ts +15 -0
- package/dist/access/module-nav.d.ts.map +1 -0
- package/dist/access/resolver.d.ts +2 -0
- package/dist/access/resolver.d.ts.map +1 -0
- package/dist/access/types.d.ts +43 -0
- package/dist/access/types.d.ts.map +1 -0
- package/dist/access/upgrade-modal-store.d.ts +23 -0
- package/dist/access/upgrade-modal-store.d.ts.map +1 -0
- package/dist/app/AdminShell.d.ts +3 -0
- package/dist/app/AdminShell.d.ts.map +1 -1
- package/dist/app/admin-app.d.ts +3 -1
- package/dist/app/admin-app.d.ts.map +1 -1
- package/dist/app/config.d.ts +47 -2
- package/dist/app/config.d.ts.map +1 -1
- package/dist/app/core-entities.d.ts +25 -0
- package/dist/app/core-entities.d.ts.map +1 -0
- package/dist/app/derive-contract.d.ts +8 -0
- package/dist/app/derive-contract.d.ts.map +1 -0
- package/dist/app/scaffold.d.ts.map +1 -1
- package/dist/archetype-lookup.d.ts.map +1 -1
- package/dist/billing/index.cjs +6 -2
- package/dist/billing/index.d.ts +2 -0
- package/dist/billing/index.d.ts.map +1 -1
- package/dist/billing/index.js +1 -1
- package/dist/billing/plan-badge.d.ts +17 -0
- package/dist/billing/plan-badge.d.ts.map +1 -0
- package/dist/billing/store.d.ts +6 -1
- package/dist/billing/store.d.ts.map +1 -1
- package/dist/{chunk-3M25GRRU.js → chunk-I3DY47N5.js} +3 -4
- package/dist/chunk-I3DY47N5.js.map +1 -0
- package/dist/chunk-NNFGMUF2.cjs +39 -0
- package/dist/chunk-NNFGMUF2.cjs.map +1 -0
- package/dist/{chunk-4XTQ4V5L.cjs → chunk-SAZLW54Y.cjs} +16 -12
- package/dist/chunk-SAZLW54Y.cjs.map +1 -0
- package/dist/{chunk-IPWCHJPE.cjs → chunk-VKYZACG5.cjs} +5 -6
- package/dist/chunk-VKYZACG5.cjs.map +1 -0
- package/dist/chunk-VN5NTK26.js +36 -0
- package/dist/chunk-VN5NTK26.js.map +1 -0
- package/dist/{chunk-XNWCGJYH.js → chunk-VTEP2AQX.js} +15 -11
- package/dist/chunk-VTEP2AQX.js.map +1 -0
- package/dist/components/shared/ContactPicker.d.ts +92 -0
- package/dist/components/shared/ContactPicker.d.ts.map +1 -0
- package/dist/components/shared/PersonLink.d.ts +12 -1
- package/dist/components/shared/PersonLink.d.ts.map +1 -1
- package/dist/components/shared/create-person.d.ts +20 -0
- package/dist/components/shared/create-person.d.ts.map +1 -0
- package/dist/crud/CrudDetailPage.d.ts.map +1 -1
- package/dist/crud/CrudFormPage.d.ts.map +1 -1
- package/dist/crud/CrudListView.d.ts.map +1 -1
- package/dist/crud/CrudPage.d.ts.map +1 -1
- package/dist/crud/archetypes/AddressesTab.d.ts +21 -0
- package/dist/crud/archetypes/AddressesTab.d.ts.map +1 -0
- package/dist/crud/archetypes/PersonDetailTabs.d.ts +2 -1
- package/dist/crud/archetypes/PersonDetailTabs.d.ts.map +1 -1
- package/dist/crud/createCrudPage.d.ts.map +1 -1
- package/dist/crud/index.d.ts +1 -0
- package/dist/crud/index.d.ts.map +1 -1
- package/dist/index.cjs +4300 -722
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4080 -542
- package/dist/index.js.map +1 -1
- package/dist/org/index.cjs +11 -11
- package/dist/org/index.js +2 -2
- package/dist/org/store.d.ts.map +1 -1
- package/dist/permissions/context.d.ts +2 -0
- package/dist/permissions/context.d.ts.map +1 -1
- package/dist/permissions/index.cjs +7 -7
- package/dist/permissions/index.js +1 -1
- package/dist/shell/components/auth/ProtectedRoute.d.ts.map +1 -1
- package/dist/shell/components/billing/SoftLimitBanner.d.ts +5 -0
- package/dist/shell/components/billing/SoftLimitBanner.d.ts.map +1 -0
- package/dist/shell/components/billing/SubscriptionPage.d.ts +5 -0
- package/dist/shell/components/billing/SubscriptionPage.d.ts.map +1 -0
- package/dist/shell/components/billing/UpgradeModal.d.ts +3 -0
- package/dist/shell/components/billing/UpgradeModal.d.ts.map +1 -0
- package/dist/shell/components/billing/UpgradePrompt.d.ts +10 -0
- package/dist/shell/components/billing/UpgradePrompt.d.ts.map +1 -0
- package/dist/shell/components/billing/access-contract.d.ts +18 -0
- package/dist/shell/components/billing/access-contract.d.ts.map +1 -0
- package/dist/shell/components/billing/gates.d.ts +14 -0
- package/dist/shell/components/billing/gates.d.ts.map +1 -0
- package/dist/shell/components/billing/index.d.ts +9 -2
- package/dist/shell/components/billing/index.d.ts.map +1 -1
- package/dist/shell/components/chat/ChatFab.d.ts +3 -1
- package/dist/shell/components/chat/ChatFab.d.ts.map +1 -1
- package/dist/shell/components/chat/ChatPanel.d.ts +3 -1
- package/dist/shell/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/shell/components/chat/ConfirmActionCard.d.ts +13 -0
- package/dist/shell/components/chat/ConfirmActionCard.d.ts.map +1 -0
- package/dist/shell/components/chat/markdown.d.ts +5 -0
- package/dist/shell/components/chat/markdown.d.ts.map +1 -0
- package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
- package/dist/shell/components/organization/TenantOnboarding.d.ts.map +1 -1
- package/dist/shell/components/organization/index.d.ts +0 -1
- package/dist/shell/components/organization/index.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedCompanySettings.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedFieldRulesSettings.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedSecuritySettings.d.ts.map +1 -1
- package/dist/shell/components/settings/LocationsCrudPage.d.ts +1 -1
- package/dist/shell/components/settings/LocationsCrudPage.d.ts.map +1 -1
- package/dist/shell/components/settings/SettingsPage.d.ts +23 -2
- package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
- package/dist/shell/hooks/index.d.ts +3 -2
- package/dist/shell/hooks/index.d.ts.map +1 -1
- package/dist/shell/hooks/useAITools.d.ts +2 -0
- package/dist/shell/hooks/useAITools.d.ts.map +1 -1
- package/dist/shell/hooks/useChat.d.ts +22 -0
- package/dist/shell/hooks/useChat.d.ts.map +1 -1
- package/dist/shell/hooks/useSystemPermission.d.ts +3 -0
- package/dist/shell/hooks/useSystemPermission.d.ts.map +1 -0
- package/dist/shell/hooks/useTenantPluginSettings.d.ts +12 -0
- package/dist/shell/hooks/useTenantPluginSettings.d.ts.map +1 -0
- package/dist/shell/lib/ai-tool-handlers.d.ts +119 -0
- package/dist/shell/lib/ai-tool-handlers.d.ts.map +1 -0
- package/dist/shell/lib/core-ai-tools.d.ts +14 -0
- package/dist/shell/lib/core-ai-tools.d.ts.map +1 -1
- package/dist/shell/lib/create-client-orders-provider.d.ts.map +1 -1
- package/dist/shell/lib/fayz-agent.d.ts +23 -0
- package/dist/shell/lib/fayz-agent.d.ts.map +1 -0
- package/dist/shell/lib/i18n.d.ts.map +1 -1
- package/dist/shell/stores/chat.store.d.ts +69 -0
- package/dist/shell/stores/chat.store.d.ts.map +1 -1
- package/dist/shell/types/billing.d.ts +7 -0
- package/dist/shell/types/billing.d.ts.map +1 -1
- package/dist/shell/types/crud.d.ts +1 -1
- package/dist/shell/types/crud.d.ts.map +1 -1
- package/dist/shell/types/plugins.d.ts +2 -45
- package/dist/shell/types/plugins.d.ts.map +1 -1
- package/dist/stores/createCrudStore.d.ts.map +1 -1
- package/package.json +15 -12
- package/src/access/context.tsx +228 -0
- package/src/access/headless.ts +71 -0
- package/src/access/index.ts +27 -0
- package/src/access/limits-registry.ts +86 -0
- package/src/access/module-nav.test.ts +78 -0
- package/src/access/module-nav.ts +82 -0
- package/src/access/resolver.test.ts +143 -0
- package/src/access/resolver.ts +5 -0
- package/src/access/types.ts +47 -0
- package/src/access/upgrade-modal-store.ts +30 -0
- package/src/app/AdminShell.tsx +451 -68
- package/src/app/admin-app.tsx +46 -9
- package/src/app/config.ts +54 -1
- package/src/app/core-entities.ts +110 -0
- package/src/app/derive-contract.test.ts +155 -0
- package/src/app/derive-contract.ts +214 -0
- package/src/app/scaffold.tsx +4 -0
- package/src/archetype-lookup.ts +8 -2
- package/src/billing/index.ts +2 -0
- package/src/billing/plan-badge.ts +32 -0
- package/src/billing/store.ts +8 -1
- package/src/components/shared/ContactPicker.tsx +394 -0
- package/src/components/shared/PersonLink.tsx +11 -3
- package/src/components/shared/create-person.ts +74 -0
- package/src/crud/CrudDetailPage.tsx +8 -1
- package/src/crud/CrudFormPage.tsx +124 -6
- package/src/crud/CrudListView.tsx +16 -1
- package/src/crud/CrudPage.tsx +27 -5
- package/src/crud/DeleteConfirmDialog.tsx +1 -1
- package/src/crud/archetypes/AddressesTab.tsx +387 -0
- package/src/crud/archetypes/PersonDetailTabs.tsx +5 -0
- package/src/crud/createCrudPage.tsx +2 -0
- package/src/crud/index.ts +6 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +49 -1
- package/src/org/store.ts +9 -1
- package/src/permissions/context.tsx +29 -15
- package/src/shell/components/auth/ProtectedRoute.tsx +12 -2
- package/src/shell/components/billing/SoftLimitBanner.tsx +81 -0
- package/src/shell/components/billing/SubscriptionPage.tsx +372 -0
- package/src/shell/components/billing/UpgradeModal.tsx +102 -0
- package/src/shell/components/billing/UpgradePrompt.tsx +229 -0
- package/src/shell/components/billing/access-contract.ts +64 -0
- package/src/shell/components/billing/gates.tsx +62 -0
- package/src/shell/components/billing/index.ts +18 -2
- package/src/shell/components/chat/ChatFab.tsx +4 -2
- package/src/shell/components/chat/ChatPanel.tsx +174 -23
- package/src/shell/components/chat/ConfirmActionCard.tsx +57 -0
- package/src/shell/components/chat/markdown.tsx +102 -0
- package/src/shell/components/organization/TeamTab.tsx +21 -4
- package/src/shell/components/organization/TenantOnboarding.tsx +3 -1
- package/src/shell/components/organization/index.ts +0 -1
- package/src/shell/components/settings/ConnectedCompanySettings.tsx +4 -1
- package/src/shell/components/settings/ConnectedFieldRulesSettings.tsx +3 -1
- package/src/shell/components/settings/ConnectedSecuritySettings.tsx +3 -1
- package/src/shell/components/settings/LocationsCrudPage.ts +6 -1
- package/src/shell/components/settings/SettingsPage.tsx +63 -21
- package/src/shell/hooks/index.ts +3 -2
- package/src/shell/hooks/useAITools.ts +27 -23
- package/src/shell/hooks/useChat.ts +381 -16
- package/src/shell/hooks/useSystemPermission.ts +23 -0
- package/src/shell/hooks/useTenantPluginSettings.ts +127 -0
- package/src/shell/lib/ai-tool-handlers.ts +910 -0
- package/src/shell/lib/core-ai-tools.ts +234 -2
- package/src/shell/lib/create-client-orders-provider.ts +24 -2
- package/src/shell/lib/fayz-agent.ts +56 -0
- package/src/shell/lib/i18n.ts +71 -0
- package/src/shell/stores/chat.store.ts +118 -1
- package/src/shell/styles.css +30 -0
- package/src/shell/types/billing.ts +8 -0
- package/src/shell/types/crud.ts +1 -1
- package/src/shell/types/plugins.ts +13 -50
- package/src/stores/createCrudStore.ts +9 -0
- package/dist/chunk-3M25GRRU.js.map +0 -1
- package/dist/chunk-4XTQ4V5L.cjs.map +0 -1
- package/dist/chunk-5GYQK5IW.js +0 -21
- package/dist/chunk-5GYQK5IW.js.map +0 -1
- package/dist/chunk-IPWCHJPE.cjs.map +0 -1
- package/dist/chunk-KYID2MR6.cjs +0 -23
- package/dist/chunk-KYID2MR6.cjs.map +0 -1
- package/dist/chunk-XNWCGJYH.js.map +0 -1
- package/dist/shell/components/billing/BillingPage.d.ts +0 -7
- package/dist/shell/components/billing/BillingPage.d.ts.map +0 -1
- package/dist/shell/components/billing/PaywallGate.d.ts +0 -11
- package/dist/shell/components/billing/PaywallGate.d.ts.map +0 -1
- package/dist/shell/components/organization/PermissionGate.d.ts +0 -11
- package/dist/shell/components/organization/PermissionGate.d.ts.map +0 -1
- package/dist/shell/hooks/useBilling.d.ts +0 -14
- package/dist/shell/hooks/useBilling.d.ts.map +0 -1
- package/dist/shell/hooks/usePermission.d.ts +0 -16
- package/dist/shell/hooks/usePermission.d.ts.map +0 -1
- package/src/shell/components/billing/BillingPage.tsx +0 -56
- package/src/shell/components/billing/PaywallGate.tsx +0 -80
- package/src/shell/components/organization/PermissionGate.tsx +0 -16
- package/src/shell/hooks/useBilling.ts +0 -151
- package/src/shell/hooks/usePermission.ts +0 -101
|
@@ -5,6 +5,7 @@ import { Button } from '@fayz-ai/ui'
|
|
|
5
5
|
import { Input } from '@fayz-ai/ui'
|
|
6
6
|
import { Checkbox } from '@fayz-ai/ui'
|
|
7
7
|
import { CurrencyInput } from '@fayz-ai/ui'
|
|
8
|
+
import { MarkdownEditor } from '@fayz-ai/ui'
|
|
8
9
|
import { toast } from '@fayz-ai/ui'
|
|
9
10
|
import { useSaveBar, useBackHandler } from '@fayz-ai/ui'
|
|
10
11
|
import { PersonFormLayout } from './archetypes/PersonFormLayout'
|
|
@@ -13,9 +14,11 @@ import { ServiceFormLayout } from './archetypes/ServiceFormLayout'
|
|
|
13
14
|
import { LocationFormLayout } from './archetypes/LocationFormLayout'
|
|
14
15
|
import { SubjectFormLayout } from './archetypes/SubjectFormLayout'
|
|
15
16
|
import { useTranslation } from '@fayz-ai/core'
|
|
17
|
+
import type { EntityImageConfig } from '@fayz-ai/core'
|
|
16
18
|
import type { FieldDef, FieldGroup, EntityDef } from '@fayz-ai/core'
|
|
17
19
|
import type { FormLayout } from '@fayz-ai/core'
|
|
18
20
|
import { RelationSelect } from './relation-field'
|
|
21
|
+
import { useLimitGuard, invalidateLimit } from '../access'
|
|
19
22
|
|
|
20
23
|
interface CrudFormPageProps {
|
|
21
24
|
entityDef: EntityDef
|
|
@@ -134,6 +137,16 @@ function renderField(field: FieldDef, value: any, onChange: (val: any) => void,
|
|
|
134
137
|
className={`${baseClass} min-h-[60px] py-1.5`}
|
|
135
138
|
/>
|
|
136
139
|
)
|
|
140
|
+
case 'markdown':
|
|
141
|
+
return (
|
|
142
|
+
<MarkdownEditor
|
|
143
|
+
value={value ?? ''}
|
|
144
|
+
onChange={onChange}
|
|
145
|
+
placeholder={field.placeholder}
|
|
146
|
+
minRows={12}
|
|
147
|
+
defaultMode="edit"
|
|
148
|
+
/>
|
|
149
|
+
)
|
|
137
150
|
case 'relation':
|
|
138
151
|
return (
|
|
139
152
|
<RelationSelect
|
|
@@ -237,16 +250,104 @@ function FormFieldItem({ field, value, onChange, allValues }: { field: FieldDef;
|
|
|
237
250
|
)
|
|
238
251
|
}
|
|
239
252
|
|
|
253
|
+
/**
|
|
254
|
+
* The image slot, wired. Renders the record's current image and, when the entity
|
|
255
|
+
* supplies an `upload`, lets it be replaced. Upload attaches the file to an
|
|
256
|
+
* existing record id, so in create mode the slot explains itself instead of
|
|
257
|
+
* offering an action that cannot work yet.
|
|
258
|
+
*/
|
|
259
|
+
function ImageSlot({
|
|
260
|
+
image,
|
|
261
|
+
row,
|
|
262
|
+
canEdit,
|
|
263
|
+
}: {
|
|
264
|
+
image: EntityImageConfig
|
|
265
|
+
row: Record<string, any>
|
|
266
|
+
canEdit: boolean
|
|
267
|
+
}) {
|
|
268
|
+
const t = useTranslation()
|
|
269
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
270
|
+
const [url, setUrl] = React.useState<string | undefined>(() => image.get(row))
|
|
271
|
+
const [busy, setBusy] = React.useState(false)
|
|
272
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
273
|
+
|
|
274
|
+
// The form mounts before the record is loaded in edit mode, so the initial
|
|
275
|
+
// value can be undefined and arrive later.
|
|
276
|
+
React.useEffect(() => { setUrl(image.get(row)) }, [image.get(row)])
|
|
277
|
+
|
|
278
|
+
const pick = () => { if (canEdit && !busy) inputRef.current?.click() }
|
|
279
|
+
|
|
280
|
+
const onFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
281
|
+
const file = e.target.files?.[0]
|
|
282
|
+
e.target.value = ''
|
|
283
|
+
if (!file || !image.upload) return
|
|
284
|
+
setBusy(true)
|
|
285
|
+
setError(null)
|
|
286
|
+
try {
|
|
287
|
+
setUrl(await image.upload(file, row))
|
|
288
|
+
} catch (err: unknown) {
|
|
289
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
290
|
+
} finally {
|
|
291
|
+
setBusy(false)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const editable = canEdit && Boolean(image.upload)
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div className="shrink-0">
|
|
299
|
+
<div
|
|
300
|
+
onClick={pick}
|
|
301
|
+
role={editable ? 'button' : undefined}
|
|
302
|
+
tabIndex={editable ? 0 : undefined}
|
|
303
|
+
onKeyDown={(e) => { if (editable && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); pick() } }}
|
|
304
|
+
title={editable ? t('crud.form.changeImage') : undefined}
|
|
305
|
+
className={`relative flex h-20 w-20 items-center justify-center overflow-hidden rounded-lg border-2 text-muted-foreground transition-colors ${
|
|
306
|
+
url ? 'border-solid border-border' : 'border-dashed border-muted'
|
|
307
|
+
} ${editable ? 'cursor-pointer hover:border-primary/30 hover:text-primary/50' : ''}`}
|
|
308
|
+
>
|
|
309
|
+
{url
|
|
310
|
+
? <img src={url} alt="" className="h-full w-full object-cover" />
|
|
311
|
+
: <ImagePlus className="h-5 w-5" />}
|
|
312
|
+
{busy && (
|
|
313
|
+
<span className="absolute inset-0 flex items-center justify-center bg-background/70 text-[10px]">
|
|
314
|
+
...
|
|
315
|
+
</span>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
{editable && (
|
|
319
|
+
<input
|
|
320
|
+
ref={inputRef}
|
|
321
|
+
type="file"
|
|
322
|
+
accept={image.accept ?? 'image/*'}
|
|
323
|
+
className="hidden"
|
|
324
|
+
onChange={onFile}
|
|
325
|
+
/>
|
|
326
|
+
)}
|
|
327
|
+
{!canEdit && (
|
|
328
|
+
<p className="mt-1 w-20 text-[10px] leading-tight text-muted-foreground">
|
|
329
|
+
{t('crud.form.imageAfterSave')}
|
|
330
|
+
</p>
|
|
331
|
+
)}
|
|
332
|
+
{error && <p className="mt-1 w-20 text-[10px] leading-tight text-destructive">{error}</p>}
|
|
333
|
+
</div>
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
240
337
|
function FormGroup({
|
|
241
338
|
group,
|
|
242
339
|
fields,
|
|
243
340
|
values,
|
|
244
341
|
onChange,
|
|
342
|
+
image,
|
|
343
|
+
canEditImage,
|
|
245
344
|
}: {
|
|
246
345
|
group: FieldGroup
|
|
247
346
|
fields: FieldDef[]
|
|
248
347
|
values: Record<string, any>
|
|
249
348
|
onChange: (key: string, val: any) => void
|
|
349
|
+
image?: EntityImageConfig
|
|
350
|
+
canEditImage?: boolean
|
|
250
351
|
}) {
|
|
251
352
|
const cols = group.columns ?? 2
|
|
252
353
|
|
|
@@ -274,12 +375,16 @@ function FormGroup({
|
|
|
274
375
|
<CardContent className="pt-4">
|
|
275
376
|
{group.imageSlot ? (
|
|
276
377
|
<div className="flex gap-4">
|
|
277
|
-
{
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
378
|
+
{image
|
|
379
|
+
? <ImageSlot image={image} row={values} canEdit={Boolean(canEditImage)} />
|
|
380
|
+
: (
|
|
381
|
+
// No entity image config: keep the old decorative square.
|
|
382
|
+
<div className="shrink-0">
|
|
383
|
+
<div className="flex h-20 w-20 items-center justify-center rounded-lg border-2 border-dashed border-muted text-muted-foreground">
|
|
384
|
+
<ImagePlus className="h-5 w-5" />
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
283
388
|
<div className="flex-1">{grid}</div>
|
|
284
389
|
</div>
|
|
285
390
|
) : (
|
|
@@ -309,7 +414,16 @@ export function CrudFormPage({ entityDef, mode, initialData, onSubmit, onCancel,
|
|
|
309
414
|
|
|
310
415
|
const computedKeys = new Set(formFields.filter((f) => f.type === 'computed').map((f) => f.key))
|
|
311
416
|
|
|
417
|
+
// Plan quantity guard for CREATE. Hook is called unconditionally (rules of
|
|
418
|
+
// hooks) with a safe empty key when the entity declares none; the guard then
|
|
419
|
+
// resolves to 'ok'. Degrades to allow-all outside <AccessProvider>.
|
|
420
|
+
const limitKey = entityDef.limitKey
|
|
421
|
+
const guardLimit = useLimitGuard(limitKey ?? '')
|
|
422
|
+
|
|
312
423
|
const submit = async () => {
|
|
424
|
+
// Client-side plan cap: block the save (the guard opens the UpgradeModal)
|
|
425
|
+
// before touching the store. Only CREATE consumes a new slot.
|
|
426
|
+
if (mode === 'create' && limitKey && (await guardLimit(1)) === 'blocked') return
|
|
313
427
|
setSaving(true)
|
|
314
428
|
try {
|
|
315
429
|
// Sanitize: convert empty strings to null so the DB doesn't choke on e.g. empty date fields.
|
|
@@ -320,6 +434,8 @@ export function CrudFormPage({ entityDef, mode, initialData, onSubmit, onCancel,
|
|
|
320
434
|
sanitized[key] = val === '' ? null : val
|
|
321
435
|
}
|
|
322
436
|
await onSubmit(sanitized)
|
|
437
|
+
// Refresh the live count so gates/banners reflect the new row immediately.
|
|
438
|
+
if (mode === 'create' && limitKey) invalidateLimit(limitKey)
|
|
323
439
|
} catch (err: any) {
|
|
324
440
|
const message = err?.message || 'Something went wrong'
|
|
325
441
|
toast.error(t('crud.form.failedToSave', { entity: entityDef.name.toLowerCase() }), { description: message })
|
|
@@ -435,6 +551,8 @@ export function CrudFormPage({ entityDef, mode, initialData, onSubmit, onCancel,
|
|
|
435
551
|
fields={fields}
|
|
436
552
|
values={values}
|
|
437
553
|
onChange={handleChange}
|
|
554
|
+
image={entityDef.image}
|
|
555
|
+
canEditImage={mode === 'edit'}
|
|
438
556
|
/>
|
|
439
557
|
)
|
|
440
558
|
})}
|
|
@@ -10,6 +10,7 @@ import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '@fayz-
|
|
|
10
10
|
import { fieldToColumns } from './fieldToColumn'
|
|
11
11
|
import { CrudCardGrid } from './CrudCardGrid'
|
|
12
12
|
import { PermissionGate } from '../permissions/PermissionGate'
|
|
13
|
+
import { LimitGate } from '../shell/components/billing/gates'
|
|
13
14
|
|
|
14
15
|
/** A resolved facet: a field plus the option pills to render. */
|
|
15
16
|
export interface CrudFacet {
|
|
@@ -112,6 +113,20 @@ export function CrudListView<T extends { id: string }>({
|
|
|
112
113
|
? <Button onClick={onNew}>{addLabel ?? t('crud.list.addEntity', { entity: entityDef.name })}</Button>
|
|
113
114
|
: null
|
|
114
115
|
|
|
116
|
+
// Compose the create action's gates: RBAC (PermissionGate, role axis) then the
|
|
117
|
+
// plan quantity cap (LimitGate, plan axis). At the cap, LimitGate dims the
|
|
118
|
+
// button and intercepts the click to open the global UpgradeModal.
|
|
119
|
+
const gatedNewButton = newButton
|
|
120
|
+
? (() => {
|
|
121
|
+
const roleGated = feature
|
|
122
|
+
? <PermissionGate feature={feature} action="create">{newButton}</PermissionGate>
|
|
123
|
+
: newButton
|
|
124
|
+
return entityDef.limitKey
|
|
125
|
+
? <LimitGate limitKey={entityDef.limitKey}>{roleGated}</LimitGate>
|
|
126
|
+
: roleGated
|
|
127
|
+
})()
|
|
128
|
+
: null
|
|
129
|
+
|
|
115
130
|
return (
|
|
116
131
|
<div className="space-y-6">
|
|
117
132
|
<div className="flex items-center justify-between">
|
|
@@ -124,7 +139,7 @@ export function CrudListView<T extends { id: string }>({
|
|
|
124
139
|
)}
|
|
125
140
|
</div>
|
|
126
141
|
<div className="flex items-center gap-2">
|
|
127
|
-
{
|
|
142
|
+
{gatedNewButton}
|
|
128
143
|
{hasImportExport && (
|
|
129
144
|
<Dropdown>
|
|
130
145
|
<DropdownTrigger asChild>
|
package/src/crud/CrudPage.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { ImportWizard } from './ImportWizard'
|
|
|
13
13
|
import { exportToCSV } from './csv-export'
|
|
14
14
|
import { fieldToColumns } from './fieldToColumn'
|
|
15
15
|
import { PermissionGate } from '../permissions/PermissionGate'
|
|
16
|
+
import { useLimitGuard, invalidateLimit } from '../access'
|
|
16
17
|
import { useFieldRules } from '../hooks/useFieldRules'
|
|
17
18
|
import { deriveEntityKey } from '@fayz-ai/core'
|
|
18
19
|
import { toast } from '@fayz-ai/ui'
|
|
@@ -196,6 +197,7 @@ export function CrudPage<T extends { id: string }>({ entityDef: rawEntityDef, us
|
|
|
196
197
|
}, [store])
|
|
197
198
|
const tanColumns = useCrudColumns(entityDef, { basePath: normalizedBasePath, onDelete: readOnly ? undefined : (item) => setDeleteItem(item), onInlineUpdate: handleInlineUpdate, feature, readOnly })
|
|
198
199
|
const currentOrgId = useOrganizationStore((s) => s.currentOrg?.id)
|
|
200
|
+
const guardImport = useLimitGuard(entityDef.limitKey ?? '')
|
|
199
201
|
|
|
200
202
|
useEffect(() => {
|
|
201
203
|
if (currentOrgId) store.fetch()
|
|
@@ -216,11 +218,25 @@ export function CrudPage<T extends { id: string }>({ entityDef: rawEntityDef, us
|
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
220
|
|
|
221
|
+
// Plan quantity guard BEFORE the batch insert: adding `rows.length` rows must
|
|
222
|
+
// stay within the plan cap. When it would exceed, the guard opens the global
|
|
223
|
+
// UpgradeModal; we also surface a clear toast + a wizard error row and abort.
|
|
224
|
+
if (entityDef.limitKey && (await guardImport(rows.length)) === 'blocked') {
|
|
225
|
+
toast.error(t('crud.import.limitExceeded'))
|
|
226
|
+
return {
|
|
227
|
+
success: 0,
|
|
228
|
+
errors: [{ row: 0, message: t('crud.import.limitExceeded') }],
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
219
232
|
const { success, results } = await store.createMany(rows, {
|
|
220
233
|
batchSize: 10,
|
|
221
234
|
onProgress,
|
|
222
235
|
})
|
|
223
236
|
|
|
237
|
+
// Refresh the live count so gates/banners reflect the imported rows.
|
|
238
|
+
if (success > 0 && entityDef.limitKey) invalidateLimit(entityDef.limitKey)
|
|
239
|
+
|
|
224
240
|
const errors: ImportRowError[] = []
|
|
225
241
|
for (let i = 0; i < results.length; i++) {
|
|
226
242
|
if (results[i].error) {
|
|
@@ -232,7 +248,7 @@ export function CrudPage<T extends { id: string }>({ entityDef: rawEntityDef, us
|
|
|
232
248
|
toast.success(t('crud.import.toastSuccess', { count: String(success) }))
|
|
233
249
|
}
|
|
234
250
|
return { success, errors }
|
|
235
|
-
}, [store, t])
|
|
251
|
+
}, [store, t, guardImport, entityDef.limitKey])
|
|
236
252
|
|
|
237
253
|
const handleExport = useCallback(() => {
|
|
238
254
|
const items = store.items
|
|
@@ -460,10 +476,16 @@ export function CrudPage<T extends { id: string }>({ entityDef: rawEntityDef, us
|
|
|
460
476
|
)
|
|
461
477
|
}
|
|
462
478
|
|
|
463
|
-
const handleDeleteConfirm = () => {
|
|
464
|
-
if (deleteItem)
|
|
465
|
-
|
|
466
|
-
|
|
479
|
+
const handleDeleteConfirm = async () => {
|
|
480
|
+
if (!deleteItem) return
|
|
481
|
+
const id = (deleteItem as any).id
|
|
482
|
+
setDeleteItem(null)
|
|
483
|
+
// Await the removal so the (cache-invalidated) list refetch completes and the
|
|
484
|
+
// deleted row is gone before we settle on the list view — previously this was
|
|
485
|
+
// fire-and-forget and raced with navigation, leaving a ghost row behind.
|
|
486
|
+
try {
|
|
487
|
+
await store.remove(id)
|
|
488
|
+
} finally {
|
|
467
489
|
navigateToList()
|
|
468
490
|
}
|
|
469
491
|
}
|
|
@@ -27,7 +27,7 @@ export function DeleteConfirmDialog({ open, onClose, onConfirm, entityName, disp
|
|
|
27
27
|
<ModalTitle>{t('crud.delete.title', { entity: entityName })}</ModalTitle>
|
|
28
28
|
<ModalDescription>
|
|
29
29
|
{displayValue
|
|
30
|
-
? t('crud.delete.confirmNamed', {
|
|
30
|
+
? t('crud.delete.confirmNamed', { displayValue })
|
|
31
31
|
: t('crud.delete.confirmGeneric', { entity: entityName.toLowerCase() })}
|
|
32
32
|
</ModalDescription>
|
|
33
33
|
</ModalHeader>
|