betterstart-cli 0.0.117 → 0.0.119

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.
Files changed (87) hide show
  1. package/dist/assets/adapters/next/templates/init/admin-globals.css +154 -38
  2. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid-row.tsx +33 -1
  3. package/dist/assets/adapters/next/templates/init/components/shared/media/media-gallery-dialog.tsx +3 -3
  4. package/dist/assets/adapters/next/templates/init/components/shared/media/media-preview.tsx +3 -1
  5. package/dist/assets/adapters/next/templates/init/hooks/use-field-error-visible.ts +25 -0
  6. package/dist/assets/adapters/next/templates/init/hooks/use-media.ts +1 -1
  7. package/dist/assets/adapters/next/templates/init/hooks/use-upload.ts +11 -5
  8. package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +1 -0
  9. package/dist/assets/adapters/next/templates/init/lib/actions/audit/query-audit-logs.ts +2 -2
  10. package/dist/assets/adapters/next/templates/init/lib/actions/audit/record-audit-event.ts +1 -2
  11. package/dist/assets/adapters/next/templates/init/lib/actions/auth/client.ts +4 -1
  12. package/dist/assets/adapters/next/templates/init/lib/actions/entity-versions/get-entity-versions.ts +5 -1
  13. package/dist/assets/adapters/next/templates/init/lib/actions/forms/get-all-form-settings.ts +1 -1
  14. package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +2 -1
  15. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +1 -0
  16. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +1 -0
  17. package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +2 -4
  18. package/dist/assets/adapters/next/templates/init/lib/actions/profile/change-own-password.ts +1 -0
  19. package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +1 -0
  20. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-files.ts +1 -0
  21. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +16 -6
  22. package/dist/assets/adapters/next/templates/init/lib/actions/users/change-user-password.ts +1 -0
  23. package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +2 -1
  24. package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +1 -0
  25. package/dist/assets/adapters/next/templates/init/lib/actions/users/types.ts +1 -1
  26. package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +1 -0
  27. package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +12 -4
  28. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/registry.ts +4 -4
  29. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +3 -7
  30. package/dist/assets/adapters/next/templates/init/pages/forgot-password-form.tsx +5 -8
  31. package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +2 -2
  32. package/dist/assets/adapters/next/templates/init/pages/login-form.tsx +7 -9
  33. package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +2 -2
  34. package/dist/assets/adapters/next/templates/init/pages/reset-password-form.tsx +8 -16
  35. package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +2 -2
  36. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-detail-row.tsx +1 -1
  37. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-table.tsx +1 -6
  38. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-table.tsx +2 -7
  39. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-table.tsx +1 -6
  40. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +1 -1
  41. package/dist/assets/adapters/next/templates/init/public/placeholder.png +0 -0
  42. package/dist/assets/adapters/next/templates/init/types/index.ts +2 -0
  43. package/dist/assets/adapters/next/templates/init/utils/audit/normalize-comparable-value.ts +4 -2
  44. package/dist/assets/adapters/next/templates/init/utils/date/format-action-date.ts +12 -0
  45. package/dist/assets/adapters/next/templates/init/utils/dev-mode/dev-mode-json-schemas.ts +66 -0
  46. package/dist/assets/adapters/next/templates/init/utils/dev-mode/parse-dev-mode-lifecycle-hooks.ts +11 -0
  47. package/dist/assets/adapters/next/templates/init/utils/dev-mode/parse-dev-mode-snippets.ts +9 -0
  48. package/dist/assets/adapters/next/templates/init/utils/editor/set-task-item-checked.ts +13 -0
  49. package/dist/assets/adapters/next/templates/init/utils/editor/update-nodes-attr.ts +2 -1
  50. package/dist/assets/adapters/next/templates/init/utils/email/email-theme.ts +13 -0
  51. package/dist/assets/adapters/next/templates/init/utils/email/render-form-submission-rows.ts +3 -2
  52. package/dist/assets/adapters/next/templates/init/utils/email/render-notification-email.ts +7 -6
  53. package/dist/assets/adapters/next/templates/init/utils/email/render-thank-you-email.ts +5 -4
  54. package/dist/assets/adapters/next/templates/init/utils/error/read-string-property.ts +4 -2
  55. package/dist/assets/adapters/next/templates/init/utils/error/recognize.ts +2 -4
  56. package/dist/assets/adapters/next/templates/init/utils/error/to-action-error.ts +0 -1
  57. package/dist/assets/adapters/next/templates/init/utils/form/clone-default-value.ts +4 -2
  58. package/dist/assets/adapters/next/templates/init/utils/shared/is-record.ts +3 -0
  59. package/dist/assets/adapters/next/templates/init/utils/table/default-get-id.ts +3 -2
  60. package/dist/assets/shared-assets/react-admin/custom/content-editor/editor-toolbar.tsx +1 -1
  61. package/dist/assets/shared-assets/react-admin/custom/content-editor/slash-command-menu.tsx +5 -3
  62. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +1 -1
  63. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-node/removable-image-node/removable-image-node.tsx +3 -1
  64. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-node/task-item-node/task-item-node-extension.tsx +6 -15
  65. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +1 -6
  66. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +1 -6
  67. package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +9 -2
  68. package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +3 -3
  69. package/dist/assets/shared-assets/react-admin/custom/media-field.tsx +2 -2
  70. package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +3 -1
  71. package/dist/assets/shared-assets/react-admin/custom/progressive-blur.tsx +3 -1
  72. package/dist/assets/shared-assets/react-admin/ui/badge.tsx +1 -2
  73. package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +20 -23
  74. package/dist/assets/shared-assets/react-admin/ui/form.tsx +6 -3
  75. package/dist/assets/shared-assets/react-admin/ui/input.tsx +1 -1
  76. package/dist/assets/shared-assets/react-admin/ui/native-select.tsx +1 -1
  77. package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +1 -1
  78. package/dist/assets/shared-assets/react-admin/ui/select.tsx +1 -1
  79. package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
  80. package/dist/{chunk-FGEVDA53.js → chunk-BNHFSHPE.js} +12 -1
  81. package/dist/chunk-BNHFSHPE.js.map +1 -0
  82. package/dist/cli.js +909 -598
  83. package/dist/cli.js.map +1 -1
  84. package/dist/{read-namespaced-template-KUENIIDC.js → read-namespaced-template-WYIGUH7S.js} +2 -2
  85. package/package.json +1 -1
  86. package/dist/chunk-FGEVDA53.js.map +0 -1
  87. /package/dist/{read-namespaced-template-KUENIIDC.js.map → read-namespaced-template-WYIGUH7S.js.map} +0 -0
@@ -1,3 +1,8 @@
1
+ import type {
2
+ AuditLogAction,
3
+ AuditLogCategory,
4
+ AuditLogSourceType
5
+ } from '@admin/actions/audit/types'
1
6
  import { sql } from 'drizzle-orm'
2
7
  import {
3
8
  boolean,
@@ -193,8 +198,8 @@ export const auditLog = pgTable(
193
198
  'AuditLog',
194
199
  {
195
200
  id: uuid().defaultRandom().primaryKey().notNull(),
196
- category: varchar('category', { length: 50 }).notNull(),
197
- action: varchar('action', { length: 50 }).notNull(),
201
+ category: varchar('category', { length: 50 }).notNull().$type<AuditLogCategory>(),
202
+ action: varchar('action', { length: 50 }).notNull().$type<AuditLogAction>(),
198
203
  targetType: varchar('target_type', { length: 100 }).notNull(),
199
204
  targetId: text('target_id'),
200
205
  targetLabel: text('target_label'),
@@ -204,14 +209,17 @@ export const auditLog = pgTable(
204
209
  actorEmail: text('actor_email'),
205
210
  actorName: text('actor_name'),
206
211
  actorRole: text('actor_role'),
207
- sourceType: varchar('source_type', { length: 50 }).notNull().default('server_action'),
212
+ sourceType: varchar('source_type', { length: 50 })
213
+ .notNull()
214
+ .default('server_action')
215
+ .$type<AuditLogSourceType>(),
208
216
  ipAddress: varchar('ip_address', { length: 100 }),
209
217
  userAgent: varchar('user_agent', { length: 500 }),
210
218
  changedFields: text('changed_fields')
211
219
  .array()
212
220
  .notNull()
213
221
  .default(sql`ARRAY[]::text[]`),
214
- metadata: jsonb('metadata'),
222
+ metadata: jsonb('metadata').$type<Record<string, unknown>>(),
215
223
  createdAt: timestamp('created_at', { precision: 3, mode: 'string' })
216
224
  .default(sql`CURRENT_TIMESTAMP`)
217
225
  .notNull()
@@ -40,11 +40,11 @@ export function registerHook<Event extends LifecycleHookEvent>(
40
40
  }
41
41
  }
42
42
 
43
- export function getRegisteredHooks<Event extends LifecycleHookEvent>(
43
+ export function getRegisteredHooks(
44
44
  entityType: string,
45
- event: Event
46
- ): LifecycleHookHandler<Event>[] {
47
- return [...((lifecycleHooks.get(entityType)?.get(event) ?? []) as LifecycleHookHandler<Event>[])]
45
+ event: LifecycleHookEvent
46
+ ): LifecycleHookHandler<LifecycleHookEvent>[] {
47
+ return [...(lifecycleHooks.get(entityType)?.get(event) ?? [])]
48
48
  }
49
49
 
50
50
  export function getLifecycleHookSummary(): Record<
@@ -5,15 +5,11 @@ import type {
5
5
  LifecycleHookAbort,
6
6
  LifecycleHookContext
7
7
  } from './types'
8
+ import { isRecord } from '@admin/utils/shared/is-record'
8
9
  import { getRegisteredAdminHooks, getRegisteredHooks } from './registry'
9
10
 
10
11
  function isLifecycleHookAbort(value: unknown): value is LifecycleHookAbort {
11
- return (
12
- typeof value === 'object' &&
13
- value !== null &&
14
- 'abort' in value &&
15
- (value as { abort: unknown }).abort === true
16
- )
12
+ return isRecord(value) && value.abort === true
17
13
  }
18
14
 
19
15
  function createAbortError(event: BeforeLifecycleHookEvent, context: LifecycleHookContext): Error {
@@ -98,7 +94,7 @@ function toAdminEventContext(
98
94
  }
99
95
 
100
96
  function runSettledHook<Context>(
101
- handler: (context: Context) => void | Promise<void>,
97
+ handler: (context: Context) => void,
102
98
  context: Context
103
99
  ): Promise<void> {
104
100
  try {
@@ -10,6 +10,7 @@ import { Field, FieldGroup, FieldLabel } from '@admin/components/ui/field'
10
10
  import { Form } from '@admin/components/ui/form'
11
11
  import { Input } from '@admin/components/ui/input'
12
12
  import { Spinner } from '@admin/components/ui/spinner'
13
+ import { useFieldErrorVisible } from '@admin/hooks/use-field-error-visible'
13
14
  import { cn } from '@admin/utils/shared/cn'
14
15
  import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
15
16
  import Image from 'next/image'
@@ -33,6 +34,8 @@ export function ForgotPasswordForm({ className, ...props }: React.ComponentProps
33
34
  reValidateMode: 'onChange'
34
35
  })
35
36
 
37
+ const isEmailErrorVisible = useFieldErrorVisible(form, 'email')
38
+
36
39
  const handleSubmit = (values: ForgotPasswordFormValues) => {
37
40
  startTransition(async () => {
38
41
  try {
@@ -68,13 +71,7 @@ export function ForgotPasswordForm({ className, ...props }: React.ComponentProps
68
71
  <Card className="overflow-hidden p-0">
69
72
  <CardContent className="grid grid-cols-[5fr_7fr] p-0">
70
73
  <div className="relative block bg-muted">
71
- <Image
72
- src="https://assets.betterstart.dev/assets/placeholder.png"
73
- alt=""
74
- fill
75
- sizes="42vw"
76
- className="object-cover"
77
- />
74
+ <Image src="/placeholder.png" alt="" fill sizes="42vw" className="object-cover" />
78
75
  </div>
79
76
  <div className="p-20">
80
77
  <Form {...form}>
@@ -99,7 +96,7 @@ export function ForgotPasswordForm({ className, ...props }: React.ComponentProps
99
96
  type="email"
100
97
  autoComplete="off"
101
98
  placeholder="m@example.com"
102
- aria-invalid={Boolean(form.formState.errors.email)}
99
+ aria-invalid={isEmailErrorVisible}
103
100
  {...form.register('email')}
104
101
  required
105
102
  disabled={isPending}
@@ -2,8 +2,8 @@ export function ForgotPasswordPageSkeleton() {
2
2
  return (
3
3
  <div className="overflow-hidden rounded-sm border bg-card">
4
4
  <div className="grid grid-cols-[5fr_7fr]">
5
- <div className="block min-h-[460px] bg-muted" />
6
- <div className="flex min-h-[460px] flex-col gap-10 p-20">
5
+ <div className="block min-h-auth-forgot-panel bg-muted" />
6
+ <div className="flex min-h-auth-forgot-panel flex-col gap-10 p-20">
7
7
  <div className="space-y-3">
8
8
  <div className="h-6 w-40 animate-pulse rounded-md bg-muted" />
9
9
  <div className="h-4 w-full max-w-72 animate-pulse rounded-md bg-muted" />
@@ -9,6 +9,7 @@ import { Field, FieldGroup, FieldLabel } from '@admin/components/ui/field'
9
9
  import { Form } from '@admin/components/ui/form'
10
10
  import { Input } from '@admin/components/ui/input'
11
11
  import { Spinner } from '@admin/components/ui/spinner'
12
+ import { useFieldErrorVisible } from '@admin/hooks/use-field-error-visible'
12
13
  import { getAdminPostLoginPath } from '@admin/utils/auth/get-admin-post-login-path'
13
14
  import { cn } from '@admin/utils/shared/cn'
14
15
  import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
@@ -41,6 +42,9 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>)
41
42
  reValidateMode: 'onChange'
42
43
  })
43
44
 
45
+ const isEmailErrorVisible = useFieldErrorVisible(form, 'email')
46
+ const isPasswordErrorVisible = useFieldErrorVisible(form, 'password')
47
+
44
48
  React.useEffect(() => {
45
49
  if (reset === 'success') {
46
50
  toast.success('Password reset. Sign in with your new password.', {
@@ -83,13 +87,7 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>)
83
87
  <Card className="overflow-hidden p-0">
84
88
  <CardContent className="grid grid-cols-[5fr_7fr] p-0">
85
89
  <div className="relative block bg-muted">
86
- <Image
87
- src="https://assets.betterstart.dev/assets/placeholder.png"
88
- alt=""
89
- fill
90
- sizes="42vw"
91
- className="object-cover"
92
- />
90
+ <Image src="/placeholder.png" alt="" fill sizes="42vw" className="object-cover" />
93
91
  </div>
94
92
  <Form {...form}>
95
93
  <form
@@ -110,7 +108,7 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>)
110
108
  type="email"
111
109
  autoComplete="off"
112
110
  placeholder="m@example.com"
113
- aria-invalid={Boolean(form.formState.errors.email)}
111
+ aria-invalid={isEmailErrorVisible}
114
112
  {...form.register('email')}
115
113
  required
116
114
  disabled={isPending}
@@ -132,7 +130,7 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>)
132
130
  type="password"
133
131
  autoComplete="off"
134
132
  placeholder="Enter your password"
135
- aria-invalid={Boolean(form.formState.errors.password)}
133
+ aria-invalid={isPasswordErrorVisible}
136
134
  {...form.register('password')}
137
135
  required
138
136
  disabled={isPending}
@@ -2,8 +2,8 @@ export function LoginPageSkeleton() {
2
2
  return (
3
3
  <div className="overflow-hidden rounded-sm border bg-card">
4
4
  <div className="grid grid-cols-[5fr_7fr]">
5
- <div className="block min-h-[540px] bg-muted" />
6
- <div className="flex min-h-[540px] flex-col gap-10 p-20">
5
+ <div className="block min-h-auth-login-panel bg-muted" />
6
+ <div className="flex min-h-auth-login-panel flex-col gap-10 p-20">
7
7
  <div className="space-y-3">
8
8
  <div className="h-6 w-32 animate-pulse rounded-md bg-muted" />
9
9
  <div className="h-4 w-40 animate-pulse rounded-md bg-muted" />
@@ -9,6 +9,7 @@ import { Field, FieldGroup, FieldLabel } from '@admin/components/ui/field'
9
9
  import { Form } from '@admin/components/ui/form'
10
10
  import { Input } from '@admin/components/ui/input'
11
11
  import { Spinner } from '@admin/components/ui/spinner'
12
+ import { useFieldErrorVisible } from '@admin/hooks/use-field-error-visible'
12
13
  import { cn } from '@admin/utils/shared/cn'
13
14
  import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
14
15
  import Image from 'next/image'
@@ -49,19 +50,16 @@ export function ResetPasswordForm({ className, ...props }: React.ComponentProps<
49
50
  reValidateMode: 'onChange'
50
51
  })
51
52
 
53
+ const isNewPasswordErrorVisible = useFieldErrorVisible(form, 'newPassword')
54
+ const isConfirmPasswordErrorVisible = useFieldErrorVisible(form, 'confirmPassword')
55
+
52
56
  if (!token) {
53
57
  return (
54
58
  <div className={cn('flex flex-col gap-6', className)} {...props}>
55
59
  <Card className="overflow-hidden p-0">
56
60
  <CardContent className="grid grid-cols-[5fr_7fr] p-0">
57
61
  <div className="relative block bg-muted">
58
- <Image
59
- src="https://assets.betterstart.dev/assets/placeholder.png"
60
- alt=""
61
- fill
62
- sizes="42vw"
63
- className="object-cover"
64
- />
62
+ <Image src="/placeholder.png" alt="" fill sizes="42vw" className="object-cover" />
65
63
  </div>
66
64
  <div className="p-20">
67
65
  <FieldGroup className="gap-10 pb-12">
@@ -114,13 +112,7 @@ export function ResetPasswordForm({ className, ...props }: React.ComponentProps<
114
112
  <Card className="overflow-hidden p-0">
115
113
  <CardContent className="grid grid-cols-[5fr_7fr] p-0">
116
114
  <div className="relative block bg-muted">
117
- <Image
118
- src="https://assets.betterstart.dev/assets/placeholder.png"
119
- alt=""
120
- fill
121
- sizes="42vw"
122
- className="object-cover"
123
- />
115
+ <Image src="/placeholder.png" alt="" fill sizes="42vw" className="object-cover" />
124
116
  </div>
125
117
  <Form {...form}>
126
118
  <form
@@ -144,7 +136,7 @@ export function ResetPasswordForm({ className, ...props }: React.ComponentProps<
144
136
  type="password"
145
137
  autoComplete="off"
146
138
  placeholder="Enter new password"
147
- aria-invalid={Boolean(form.formState.errors.newPassword)}
139
+ aria-invalid={isNewPasswordErrorVisible}
148
140
  {...form.register('newPassword')}
149
141
  required
150
142
  disabled={isPending}
@@ -157,7 +149,7 @@ export function ResetPasswordForm({ className, ...props }: React.ComponentProps<
157
149
  type="password"
158
150
  autoComplete="off"
159
151
  placeholder="Confirm new password"
160
- aria-invalid={Boolean(form.formState.errors.confirmPassword)}
152
+ aria-invalid={isConfirmPasswordErrorVisible}
161
153
  {...form.register('confirmPassword')}
162
154
  required
163
155
  disabled={isPending}
@@ -2,8 +2,8 @@ export function ResetPasswordPageSkeleton() {
2
2
  return (
3
3
  <div className="overflow-hidden rounded-sm border bg-card">
4
4
  <div className="grid grid-cols-[5fr_7fr]">
5
- <div className="block min-h-[520px] bg-muted" />
6
- <div className="flex min-h-[520px] flex-col gap-10 p-20">
5
+ <div className="block min-h-auth-reset-panel bg-muted" />
6
+ <div className="flex min-h-auth-reset-panel flex-col gap-10 p-20">
7
7
  <div className="space-y-3">
8
8
  <div className="h-6 w-36 animate-pulse rounded-md bg-muted" />
9
9
  <div className="h-4 w-56 animate-pulse rounded-md bg-muted" />
@@ -7,7 +7,7 @@ interface AuditLogDetailRowProps {
7
7
 
8
8
  export function AuditLogDetailRow({ label, value }: AuditLogDetailRowProps) {
9
9
  return (
10
- <div className="grid grid-cols-[120px_1fr] gap-3 border-b border-border px-4 py-2 last:border-b-0">
10
+ <div className="grid grid-cols-audit-detail gap-3 border-b border-border px-4 py-2 last:border-b-0">
11
11
  <dt className="text-sm font-medium text-muted-foreground">{label}</dt>
12
12
  <dd className="min-w-0 text-sm break-words text-foreground">{value || 'Unknown'}</dd>
13
13
  </div>
@@ -248,12 +248,7 @@ export function AuditLogTable<TValue>({
248
248
  <DataGridRow
249
249
  key={row.id}
250
250
  className="cursor-pointer hover:bg-muted/50"
251
- onClick={(event) => {
252
- const target = event.target as HTMLElement
253
- if (!event.currentTarget.contains(target)) return
254
- if (target.closest('button, a, input, [role="checkbox"]')) return
255
- onRowClick(row.original)
256
- }}
251
+ onActivate={() => onRowClick(row.original)}
257
252
  >
258
253
  {row.getVisibleCells().map((cell) => (
259
254
  <DataGridCell
@@ -36,7 +36,7 @@ export function FormsSettingsTable({
36
36
  <div className={cn('flex h-full flex-col gap-6 pt-px pb-2', className)}>
37
37
  <DataGrid
38
38
  containerClassName="min-h-0 flex-1"
39
- gridTemplateColumns="minmax(200px, 1fr) minmax(240px, 2fr)"
39
+ gridTemplateColumns="minmax(var(--spacing-form-settings-name), 1fr) minmax(var(--spacing-form-settings-recipients), 2fr)"
40
40
  >
41
41
  <DataGridHeader>
42
42
  {table.getHeaderGroups().map((headerGroup) => (
@@ -57,12 +57,7 @@ export function FormsSettingsTable({
57
57
  <DataGridRow
58
58
  key={row.id}
59
59
  className="cursor-pointer hover:bg-muted/50"
60
- onClick={(event) => {
61
- const target = event.target as HTMLElement
62
- if (!event.currentTarget.contains(target)) return
63
- if (target.closest('button, a, input, [role="checkbox"]')) return
64
- onRowClick(row.original)
65
- }}
60
+ onActivate={() => onRowClick(row.original)}
66
61
  >
67
62
  {row.getVisibleCells().map((cell) => (
68
63
  <DataGridCell key={cell.id} data-column-id={cell.column.id}>
@@ -217,12 +217,7 @@ export function WebhooksTable<TValue>({
217
217
  <DataGridRow
218
218
  key={row.id}
219
219
  className="cursor-pointer hover:bg-muted/50"
220
- onClick={(e) => {
221
- const target = e.target as HTMLElement
222
- if (!e.currentTarget.contains(target)) return
223
- if (target.closest('button, a, input, [role="checkbox"], [role="switch"]')) return
224
- onRowClick(row.original)
225
- }}
220
+ onActivate={() => onRowClick(row.original)}
226
221
  >
227
222
  {row.getVisibleCells().map((cell) => (
228
223
  <DataGridCell
@@ -118,7 +118,7 @@ export function UsersTable<TValue>({
118
118
  email: session.user.email,
119
119
  name: session.user.name,
120
120
  image: session.user.image,
121
- role: (session.user as { role?: string }).role || 'member'
121
+ role: session.user.role || 'member'
122
122
  }
123
123
  : null
124
124
  },
@@ -23,6 +23,8 @@ export interface AdminMedia {
23
23
  updatedBy: string | null
24
24
  createdByUser?: AdminUserSummary | null
25
25
  updatedByUser?: AdminUserSummary | null
26
+ createdAtFormatted?: string
27
+ updatedAtFormatted?: string
26
28
  }
27
29
 
28
30
  export interface MediaListParams {
@@ -1,10 +1,12 @@
1
+ import { isRecord } from '@admin/utils/shared/is-record'
2
+
1
3
  export function normalizeComparableValue(value: unknown): string {
2
4
  if (value instanceof Date) return value.toISOString()
3
5
  if (Array.isArray(value)) {
4
6
  return JSON.stringify(value.map((item) => normalizeComparableValue(item)))
5
7
  }
6
- if (value && typeof value === 'object') {
7
- const entries = Object.entries(value as Record<string, unknown>)
8
+ if (isRecord(value)) {
9
+ const entries = Object.entries(value)
8
10
  .sort(([a], [b]) => a.localeCompare(b))
9
11
  .map(([key, item]) => [key, normalizeComparableValue(item)])
10
12
  return JSON.stringify(entries)
@@ -0,0 +1,12 @@
1
+ export function formatActionDate(value: unknown): string {
2
+ if (typeof value !== 'string') return ''
3
+ const date = new Date(value)
4
+ if (Number.isNaN(date.getTime())) return ''
5
+
6
+ return date.toLocaleDateString('en-US', {
7
+ month: 'short',
8
+ day: '2-digit',
9
+ year: 'numeric',
10
+ timeZone: 'UTC'
11
+ })
12
+ }
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod/v3'
2
+
3
+ export const devModeCodeLanguageSchema = z.enum(['tsx', 'ts', 'json'])
4
+
5
+ export const devModeSnippetIconSchema = z.enum([
6
+ 'eye',
7
+ 'file-code',
8
+ 'inbox',
9
+ 'list',
10
+ 'pencil',
11
+ 'plus',
12
+ 'save',
13
+ 'send',
14
+ 'trash'
15
+ ])
16
+
17
+ export const devModeSnippetExampleSchema = z.object({
18
+ label: z.string(),
19
+ language: devModeCodeLanguageSchema,
20
+ code: z.string()
21
+ })
22
+
23
+ export const devModeSnippetCodeTabSchema = z.object({
24
+ id: z.string(),
25
+ label: z.string(),
26
+ language: devModeCodeLanguageSchema,
27
+ code: z.string()
28
+ })
29
+
30
+ export const devModeSnippetSchema = z.object({
31
+ id: z.string(),
32
+ label: z.string(),
33
+ kind: z.enum(['jsx', 'query', 'mutation']),
34
+ icon: devModeSnippetIconSchema,
35
+ language: devModeCodeLanguageSchema,
36
+ code: z.string(),
37
+ codeTabs: z.array(devModeSnippetCodeTabSchema).optional(),
38
+ payloadExample: devModeSnippetExampleSchema.optional(),
39
+ responseExample: devModeSnippetExampleSchema.optional()
40
+ })
41
+
42
+ export const devModeLifecycleHookExampleSchema = z.object({
43
+ id: z.string(),
44
+ label: z.string(),
45
+ event: z.enum([
46
+ 'beforeCreate',
47
+ 'afterCreate',
48
+ 'beforeUpdate',
49
+ 'afterUpdate',
50
+ 'beforeDelete',
51
+ 'afterDelete',
52
+ 'beforePublish',
53
+ 'afterPublish',
54
+ 'beforeUnpublish',
55
+ 'afterUnpublish',
56
+ 'beforeRestore',
57
+ 'afterRestore',
58
+ 'beforeReorder',
59
+ 'afterReorder'
60
+ ]),
61
+ phase: z.enum(['before', 'after']),
62
+ icon: devModeSnippetIconSchema,
63
+ language: devModeCodeLanguageSchema,
64
+ code: z.string(),
65
+ contextExample: devModeSnippetExampleSchema
66
+ })
@@ -0,0 +1,11 @@
1
+ import type { DevModeLifecycleHookExample } from '@admin/components/shared/dev-mode/dev-mode-types'
2
+ import { devModeLifecycleHookExampleSchema } from '@admin/utils/dev-mode/dev-mode-json-schemas'
3
+ import { z } from 'zod/v3'
4
+
5
+ const lifecycleHooksSchema = z.array(devModeLifecycleHookExampleSchema)
6
+
7
+ export function parseDevModeLifecycleHooks<Raw>(
8
+ values: readonly Raw[]
9
+ ): DevModeLifecycleHookExample[] {
10
+ return lifecycleHooksSchema.parse(values)
11
+ }
@@ -0,0 +1,9 @@
1
+ import type { DevModeSnippet } from '@admin/components/shared/dev-mode/dev-mode-types'
2
+ import { devModeSnippetSchema } from '@admin/utils/dev-mode/dev-mode-json-schemas'
3
+ import { z } from 'zod/v3'
4
+
5
+ const snippetsSchema = z.array(devModeSnippetSchema)
6
+
7
+ export function parseDevModeSnippets<Raw>(values: readonly Raw[]): DevModeSnippet[] {
8
+ return snippetsSchema.parse(values)
9
+ }
@@ -0,0 +1,13 @@
1
+ import type { Transaction } from '@tiptap/pm/state'
2
+
3
+ export function setTaskItemChecked(
4
+ tr: Transaction,
5
+ position: number | undefined,
6
+ checked: boolean
7
+ ): boolean {
8
+ if (typeof position !== 'number') return false
9
+
10
+ const taskItem = tr.doc.nodeAt(position)
11
+ tr.setNodeMarkup(position, undefined, { ...taskItem?.attrs, checked })
12
+ return true
13
+ }
@@ -15,7 +15,8 @@ export function updateNodesAttr<A extends string = string, V = unknown>(
15
15
  const currentNode = tr.doc.nodeAt(pos)
16
16
  if (!currentNode) continue
17
17
 
18
- const prevValue = (currentNode.attrs as Record<string, unknown>)[attrName] as V | undefined
18
+ const attrs: Record<string, unknown> = currentNode.attrs
19
+ const prevValue = attrs[attrName] as V | undefined
19
20
  const resolvedNext =
20
21
  typeof next === 'function' ? (next as (p: V | undefined) => V | undefined)(prevValue) : next
21
22
 
@@ -0,0 +1,13 @@
1
+ export const emailTheme = {
2
+ pageBackground: '#f8fafc',
3
+ surface: '#ffffff',
4
+ border: '#e2e8f0',
5
+ headerBackground: '#0f172a',
6
+ text: '#0f172a',
7
+ bodyText: '#334155',
8
+ mutedText: '#475569',
9
+ inverseText: '#ffffff',
10
+ mutedInverseText: '#cbd5e1',
11
+ compactWidth: '560px',
12
+ contentWidth: '640px'
13
+ } as const
@@ -1,3 +1,4 @@
1
+ import { emailTheme } from '@admin/utils/email/email-theme'
1
2
  import { escapeHtml } from '@admin/utils/email/escape-html'
2
3
  import { formatEmailValue } from '@admin/utils/email/format-email-value'
3
4
 
@@ -8,8 +9,8 @@ export function renderFormSubmissionRows(submissionData: Record<string, unknown>
8
9
  const displayValue = escapeHtml(formatEmailValue(value))
9
10
 
10
11
  return `<tr>
11
- <th style="border-bottom:1px solid #e2e8f0;color:#475569;font-size:12px;font-weight:600;padding:12px;text-align:left;text-transform:uppercase;vertical-align:top;width:36%;">${label}</th>
12
- <td style="border-bottom:1px solid #e2e8f0;color:#0f172a;font-size:14px;line-height:20px;padding:12px;vertical-align:top;">${displayValue}</td>
12
+ <th style="border-bottom:1px solid ${emailTheme.border};color:${emailTheme.mutedText};font-size:12px;font-weight:600;padding:12px;text-align:left;text-transform:uppercase;vertical-align:top;width:36%;">${label}</th>
13
+ <td style="border-bottom:1px solid ${emailTheme.border};color:${emailTheme.text};font-size:14px;line-height:20px;padding:12px;vertical-align:top;">${displayValue}</td>
13
14
  </tr>`
14
15
  })
15
16
  .join('\n')
@@ -1,4 +1,5 @@
1
1
  import type { FormEmailDeliveryInput } from '@admin/utils/email/form-email-delivery-input'
2
+ import { emailTheme } from '@admin/utils/email/email-theme'
2
3
  import { escapeHtml } from '@admin/utils/email/escape-html'
3
4
  import { renderFormSubmissionRows } from '@admin/utils/email/render-form-submission-rows'
4
5
 
@@ -10,15 +11,15 @@ export function renderNotificationEmail(input: FormEmailDeliveryInput): string {
10
11
 
11
12
  return `<!doctype html>
12
13
  <html>
13
- <body style="margin:0;background:#f8fafc;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#0f172a;">
14
- <div style="margin:0 auto;max-width:640px;padding:40px 20px;">
15
- <div style="border-radius:12px;background:#ffffff;border:1px solid #e2e8f0;overflow:hidden;">
16
- <div style="background:#0f172a;color:#ffffff;padding:24px 28px;">
17
- <p style="margin:0;color:#cbd5e1;font-size:13px;line-height:20px;">${formName}</p>
14
+ <body style="margin:0;background:${emailTheme.pageBackground};font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:${emailTheme.text};">
15
+ <div style="margin:0 auto;max-width:${emailTheme.contentWidth};padding:40px 20px;">
16
+ <div style="border-radius:12px;background:${emailTheme.surface};border:1px solid ${emailTheme.border};overflow:hidden;">
17
+ <div style="background:${emailTheme.headerBackground};color:${emailTheme.inverseText};padding:24px 28px;">
18
+ <p style="margin:0;color:${emailTheme.mutedInverseText};font-size:13px;line-height:20px;">${formName}</p>
18
19
  <h1 style="margin:4px 0 0;font-size:24px;line-height:32px;">New ${formLabel} submission</h1>
19
20
  </div>
20
21
  <div style="padding:24px 28px;">
21
- <p style="margin:0 0 20px;color:#475569;font-size:14px;line-height:20px;">Submitted at ${submittedAt}</p>
22
+ <p style="margin:0 0 20px;color:${emailTheme.mutedText};font-size:14px;line-height:20px;">Submitted at ${submittedAt}</p>
22
23
  <table cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;width:100%;">
23
24
  <tbody>
24
25
  ${rows}
@@ -1,4 +1,5 @@
1
1
  import type { FormEmailDeliveryInput } from '@admin/utils/email/form-email-delivery-input'
2
+ import { emailTheme } from '@admin/utils/email/email-theme'
2
3
  import { escapeHtml } from '@admin/utils/email/escape-html'
3
4
 
4
5
  export function renderThankYouEmail(input: FormEmailDeliveryInput): string {
@@ -6,11 +7,11 @@ export function renderThankYouEmail(input: FormEmailDeliveryInput): string {
6
7
 
7
8
  return `<!doctype html>
8
9
  <html>
9
- <body style="margin:0;background:#f8fafc;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#0f172a;">
10
- <div style="margin:0 auto;max-width:560px;padding:40px 20px;">
11
- <div style="border-radius:12px;background:#ffffff;padding:32px;border:1px solid #e2e8f0;">
10
+ <body style="margin:0;background:${emailTheme.pageBackground};font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:${emailTheme.text};">
11
+ <div style="margin:0 auto;max-width:${emailTheme.compactWidth};padding:40px 20px;">
12
+ <div style="border-radius:12px;background:${emailTheme.surface};padding:32px;border:1px solid ${emailTheme.border};">
12
13
  <h1 style="margin:0 0 16px;font-size:24px;line-height:32px;">Thank you</h1>
13
- <p style="margin:0;color:#334155;font-size:16px;line-height:24px;">We received your ${formLabel} submission and will get back to you shortly.</p>
14
+ <p style="margin:0;color:${emailTheme.bodyText};font-size:16px;line-height:24px;">We received your ${formLabel} submission and will get back to you shortly.</p>
14
15
  </div>
15
16
  </div>
16
17
  </body>
@@ -1,5 +1,7 @@
1
+ import { isRecord } from '@admin/utils/shared/is-record'
2
+
1
3
  export function readStringProperty(value: unknown, key: string): string | undefined {
2
- if (typeof value !== 'object' || value === null) return undefined
3
- const property = (value as Record<string, unknown>)[key]
4
+ if (!isRecord(value)) return undefined
5
+ const property = value[key]
4
6
  return typeof property === 'string' ? property : undefined
5
7
  }
@@ -1,4 +1,5 @@
1
1
  import { readStringProperty } from '@admin/utils/error/read-string-property'
2
+ import { isRecord } from '@admin/utils/shared/is-record'
2
3
 
3
4
  const POSTGRES_MESSAGES: Record<string, string> = {
4
5
  '23502': 'A required value is missing.',
@@ -13,10 +14,7 @@ const AUTH_MESSAGES: Record<string, string> = {
13
14
  }
14
15
 
15
16
  export function recognize(error: unknown): string | undefined {
16
- const authCode = readStringProperty(
17
- (error as { body?: unknown } | null)?.body ?? undefined,
18
- 'code'
19
- )
17
+ const authCode = readStringProperty(isRecord(error) ? error.body : undefined, 'code')
20
18
  if (authCode && AUTH_MESSAGES[authCode]) return AUTH_MESSAGES[authCode]
21
19
 
22
20
  const sqlState = readStringProperty(error, 'code')
@@ -1,6 +1,5 @@
1
1
  import { recognize } from '@admin/utils/error/recognize'
2
2
 
3
3
  export function toActionError(error: unknown, fallback: string): string {
4
- console.error(error)
5
4
  return recognize(error) ?? fallback
6
5
  }