@plutocms/supabase 0.14.1 → 0.15.1

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 (51) hide show
  1. package/.prettierignore +6 -0
  2. package/CHANGELOG.md +23 -0
  3. package/app/components/access/DeleteRoleModal.vue +17 -4
  4. package/app/components/access/EditRolesSlideover.vue +11 -5
  5. package/app/components/access/InviteModal.vue +44 -19
  6. package/app/components/access/LinkField.vue +14 -3
  7. package/app/components/access/LinkModal.vue +5 -1
  8. package/app/components/access/RevokeModal.vue +17 -4
  9. package/app/components/access/RoleSlideover.vue +32 -11
  10. package/app/components/mail/AuthEmailsPanel.vue +87 -20
  11. package/app/components/migrations/MigrationsBanner.vue +5 -4
  12. package/app/composables/migrations.ts +13 -6
  13. package/app/pages/admin/(auth)/accept-invite.vue +21 -5
  14. package/app/pages/admin/(auth)/forgot-password.vue +11 -4
  15. package/app/pages/admin/(auth)/signup.vue +10 -9
  16. package/app/pages/admin/(auth)/update-password.vue +8 -3
  17. package/app/pages/admin/migrations.vue +8 -3
  18. package/app/pages/admin/settings/roles.vue +58 -15
  19. package/app/pages/admin/settings/users.vue +105 -30
  20. package/app/pages/admin/setup.vue +26 -7
  21. package/app/plugins/pluto-extension.ts +21 -8
  22. package/app/utils/access-ui.ts +29 -8
  23. package/app/utils/api-failure.ts +3 -1
  24. package/app/utils/auth-driver.ts +1 -3
  25. package/app/utils/auth-error.ts +1 -1
  26. package/app/utils/invite-link.ts +3 -3
  27. package/app/utils/supabase-project.ts +8 -3
  28. package/modules/pluto-migrations.ts +38 -74
  29. package/package.json +5 -3
  30. package/server/api/_pluto/supabase/access/users/[id]/resend.post.ts +15 -5
  31. package/server/api/_pluto/supabase/access/users/[id]/roles.put.ts +4 -1
  32. package/server/api/_pluto/supabase/access/users/invite-link.post.ts +12 -3
  33. package/server/api/_pluto/supabase/access/users/invite.post.ts +7 -2
  34. package/server/api/_pluto/supabase/auth/signup.post.ts +5 -1
  35. package/server/api/_pluto/supabase/me/capabilities.get.ts +4 -1
  36. package/server/api/_pluto/supabase/migrations/run.post.ts +9 -2
  37. package/server/api/_pluto/supabase/setup/index.post.ts +4 -1
  38. package/server/utils/access-errors.ts +2 -1
  39. package/server/utils/access-invite.ts +1 -1
  40. package/server/utils/access-mail.ts +72 -28
  41. package/server/utils/capability-guard.ts +5 -2
  42. package/server/utils/content-adapter.ts +75 -24
  43. package/server/utils/migrations.ts +0 -0
  44. package/server/utils/pending-migrations.ts +0 -0
  45. package/server/utils/service-client.ts +3 -1
  46. package/server/utils/sign-in-link.ts +25 -9
  47. package/server/utils/typegen.ts +6 -4
  48. package/server/utils/validate.ts +4 -1
  49. package/shared/types/migrations.d.ts +3 -4
  50. package/shared/types/supabase.ts +62 -65
  51. package/tailwind.lint.css +9 -0
@@ -25,8 +25,14 @@ async function submitForm() {
25
25
 
26
26
  <template>
27
27
  <PlutoAdminAuthCard
28
- :title="isEmailSent ? t('supabase.auth.forgot.sentTitle') : t('supabase.auth.forgot.title')"
29
- :description="isEmailSent ? undefined : t('supabase.auth.forgot.description')"
28
+ :title="
29
+ isEmailSent
30
+ ? t('supabase.auth.forgot.sentTitle')
31
+ : t('supabase.auth.forgot.title')
32
+ "
33
+ :description="
34
+ isEmailSent ? undefined : t('supabase.auth.forgot.description')
35
+ "
30
36
  >
31
37
  <div v-if="isEmailSent" class="space-y-6 py-10">
32
38
  <div class="text-center space-y-4">
@@ -37,7 +43,9 @@ async function submitForm() {
37
43
  </hgroup>
38
44
 
39
45
  <p>
40
- <SupabaseMessageParts :message="t('supabase.auth.forgot.sentDescription')">
46
+ <SupabaseMessageParts
47
+ :message="t('supabase.auth.forgot.sentDescription')"
48
+ >
41
49
  <template #email>
42
50
  <strong>{{ email }}</strong>
43
51
  </template>
@@ -54,7 +62,6 @@ async function submitForm() {
54
62
 
55
63
  <form v-else @submit.prevent="submitForm">
56
64
  <div class="flex flex-col gap-y-6">
57
-
58
65
  <UFormField :label="t('supabase.auth.fields.email')">
59
66
  <UInput
60
67
  v-model="email"
@@ -62,8 +62,10 @@ async function submitForm() {
62
62
  description: resolveAuthErrorText(
63
63
  {
64
64
  code: failureData(error)?.code,
65
- message: (error as { data?: { statusMessage?: string } }).data?.statusMessage
66
- ?? errorText(error, 'supabase.auth.errors.unknown'),
65
+ message:
66
+ (error as { data?: { statusMessage?: string } }).data
67
+ ?.statusMessage ??
68
+ errorText(error, 'supabase.auth.errors.unknown'),
67
69
  },
68
70
  { t, te }
69
71
  ),
@@ -78,7 +80,11 @@ async function submitForm() {
78
80
 
79
81
  <template>
80
82
  <PlutoAdminAuthCard
81
- :title="isEmailVerificationMessageVisible ? t('supabase.auth.signup.almostThere') : t('supabase.auth.signup.title')"
83
+ :title="
84
+ isEmailVerificationMessageVisible
85
+ ? t('supabase.auth.signup.almostThere')
86
+ : t('supabase.auth.signup.title')
87
+ "
82
88
  >
83
89
  <div v-if="isEmailVerificationMessageVisible" class="space-y-6 py-10">
84
90
  <div class="text-center space-y-4">
@@ -98,11 +104,7 @@ async function submitForm() {
98
104
  </template>
99
105
  </UAlert>
100
106
 
101
- <UAlert
102
- icon="lucide:alert-triangle"
103
- color="warning"
104
- variant="outline"
105
- >
107
+ <UAlert icon="lucide:alert-triangle" color="warning" variant="outline">
106
108
  <template #description>
107
109
  {{ t('supabase.auth.signup.checkSpam') }}
108
110
  </template>
@@ -112,7 +114,6 @@ async function submitForm() {
112
114
 
113
115
  <form v-else @submit.prevent="submitForm">
114
116
  <div class="flex flex-col gap-y-6">
115
-
116
117
  <UFormField :label="t('supabase.auth.fields.email')" required>
117
118
  <UInput
118
119
  v-model="form.email"
@@ -51,8 +51,14 @@ async function submitForm() {
51
51
 
52
52
  <template>
53
53
  <PlutoAdminAuthCard
54
- :title="isPasswordUpdated ? t('supabase.auth.update.doneTitle') : t('supabase.auth.update.title')"
55
- :description="isPasswordUpdated ? undefined : t('supabase.auth.update.description')"
54
+ :title="
55
+ isPasswordUpdated
56
+ ? t('supabase.auth.update.doneTitle')
57
+ : t('supabase.auth.update.title')
58
+ "
59
+ :description="
60
+ isPasswordUpdated ? undefined : t('supabase.auth.update.description')
61
+ "
56
62
  >
57
63
  <div v-if="isPasswordUpdated" class="space-y-6 py-10">
58
64
  <div class="text-center space-y-4">
@@ -76,7 +82,6 @@ async function submitForm() {
76
82
 
77
83
  <form v-else @submit.prevent="submitForm">
78
84
  <div class="flex flex-col gap-y-6">
79
-
80
85
  <UFormField :label="t('supabase.auth.update.newPassword')">
81
86
  <UInput
82
87
  v-model="form.password"
@@ -189,7 +189,9 @@ async function applyMigrations(useForm: boolean) {
189
189
 
190
190
  <template>
191
191
  <AdminView>
192
- <h1 class="text-3xl font-bold lg:text-4xl">{{ t('supabase.migrations.title') }}</h1>
192
+ <h1 class="text-3xl font-bold lg:text-4xl">
193
+ {{ t('supabase.migrations.title') }}
194
+ </h1>
193
195
 
194
196
  <UAlert
195
197
  v-if="isReconnecting"
@@ -266,7 +268,10 @@ async function applyMigrations(useForm: boolean) {
266
268
  class="flex flex-col gap-y-4"
267
269
  @submit="applyMigrations(true)"
268
270
  >
269
- <UFormField :label="t('supabase.connection.connectionString')" required>
271
+ <UFormField
272
+ :label="t('supabase.connection.connectionString')"
273
+ required
274
+ >
270
275
  <UInput
271
276
  v-model="connectionForm.connectionString"
272
277
  :disabled="isRunning"
@@ -388,7 +393,7 @@ async function applyMigrations(useForm: boolean) {
388
393
  :key="`${result.layerName}/${result.migrationName}`"
389
394
  >
390
395
  <span class="font-mono"
391
- >{{ result.layerName }}/{{ result.migrationName }}</span
396
+ >{{ result.layerName }}/{{ result.migrationName }}</span
392
397
  >
393
398
  — {{ statusText(result.status) }}
394
399
  <span v-if="result.error" class="text-error">
@@ -7,15 +7,20 @@ import type { AccessRole } from '../../../../shared/types/access'
7
7
  import { ADMIN_ROLE_KEY } from '../../../utils/access-ui'
8
8
 
9
9
  // The slideover and the modal load on demand. They stay out of the page chunk.
10
- const RoleSlideover = defineAsyncComponent(() => import('../../../components/access/RoleSlideover.vue'))
11
- const DeleteRoleModal = defineAsyncComponent(() => import('../../../components/access/DeleteRoleModal.vue'))
10
+ const RoleSlideover = defineAsyncComponent(
11
+ () => import('../../../components/access/RoleSlideover.vue')
12
+ )
13
+ const DeleteRoleModal = defineAsyncComponent(
14
+ () => import('../../../components/access/DeleteRoleModal.vue')
15
+ )
12
16
 
13
17
  const { t } = usePlutoI18n()
14
18
  const overlay = useOverlay()
15
19
  const errorText = usePlutoErrorText()
16
20
 
17
21
  useHead({
18
- title: () => `${t('supabase.access.roles.title')} - ${t('pluto.pages.settings')}`,
22
+ title: () =>
23
+ `${t('supabase.access.roles.title')} - ${t('pluto.pages.settings')}`,
19
24
  })
20
25
 
21
26
  const roles = ref<AccessRole[]>([])
@@ -29,7 +34,9 @@ async function load() {
29
34
  }
30
35
 
31
36
  try {
32
- const response = await $fetch<{ roles: AccessRole[] }>('/api/_pluto/supabase/access/roles')
37
+ const response = await $fetch<{ roles: AccessRole[] }>(
38
+ '/api/_pluto/supabase/access/roles'
39
+ )
33
40
 
34
41
  roles.value = response.roles
35
42
  status.value = 'ready'
@@ -77,7 +84,9 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
77
84
  const readOnly = role.key === ADMIN_ROLE_KEY
78
85
  const main: DropdownMenuItem[] = [
79
86
  {
80
- label: readOnly ? t('supabase.access.roles.menu.view') : t('supabase.access.roles.menu.edit'),
87
+ label: readOnly
88
+ ? t('supabase.access.roles.menu.view')
89
+ : t('supabase.access.roles.menu.edit'),
81
90
  icon: readOnly ? 'lucide:eye' : 'lucide:pencil',
82
91
  onSelect: () => openRole(role),
83
92
  },
@@ -106,8 +115,12 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
106
115
  <div class="flex flex-col gap-y-6">
107
116
  <div class="flex flex-wrap items-start justify-between gap-4">
108
117
  <div>
109
- <h2 class="text-xl font-semibold">{{ t('supabase.access.roles.title') }}</h2>
110
- <p class="text-muted text-sm">{{ t('supabase.access.roles.description') }}</p>
118
+ <h2 class="text-xl font-semibold">
119
+ {{ t('supabase.access.roles.title') }}
120
+ </h2>
121
+ <p class="text-muted text-sm">
122
+ {{ t('supabase.access.roles.description') }}
123
+ </p>
111
124
  </div>
112
125
 
113
126
  <UButton icon="lucide:plus" @click="openRole(null)">
@@ -116,7 +129,11 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
116
129
  </div>
117
130
 
118
131
  <!-- Loading -->
119
- <div v-if="status === 'loading'" class="flex flex-col gap-y-3" aria-busy="true">
132
+ <div
133
+ v-if="status === 'loading'"
134
+ class="flex flex-col gap-y-3"
135
+ aria-busy="true"
136
+ >
120
137
  <USkeleton v-for="row in 3" :key="row" class="h-12 w-full" />
121
138
  </div>
122
139
 
@@ -125,18 +142,32 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
125
142
  v-else-if="status === 'error'"
126
143
  :title="t('supabase.access.roles.error.title')"
127
144
  :description="loadError"
128
- :actions="[{ label: t('pluto.common.tryAgain'), color: 'neutral', variant: 'outline', onClick: load }]"
145
+ :actions="[
146
+ {
147
+ label: t('pluto.common.tryAgain'),
148
+ color: 'neutral',
149
+ variant: 'outline',
150
+ onClick: load,
151
+ },
152
+ ]"
129
153
  color="error"
130
154
  variant="subtle"
131
155
  icon="lucide:circle-x"
132
156
  />
133
157
 
134
158
  <!-- The list -->
135
- <UTable v-else :data="roles" :columns="columns" class="border-default rounded-lg border">
159
+ <UTable
160
+ v-else
161
+ :data="roles"
162
+ :columns="columns"
163
+ class="border-default rounded-lg border"
164
+ >
136
165
  <template #role-cell="{ row }">
137
166
  <div class="flex min-w-0 flex-col">
138
167
  <span class="flex items-center gap-2">
139
- <span class="text-highlighted truncate font-medium">{{ row.original.label }}</span>
168
+ <span class="text-highlighted truncate font-medium">{{
169
+ row.original.label
170
+ }}</span>
140
171
  <UBadge
141
172
  v-if="row.original.is_builtin"
142
173
  :label="t('supabase.access.roles.builtin')"
@@ -145,7 +176,9 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
145
176
  size="sm"
146
177
  />
147
178
  </span>
148
- <code class="text-muted truncate font-mono text-xs">{{ row.original.key }}</code>
179
+ <code class="text-muted truncate font-mono text-xs">{{
180
+ row.original.key
181
+ }}</code>
149
182
  </div>
150
183
  </template>
151
184
 
@@ -154,17 +187,27 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
154
187
  {{ t('supabase.access.roles.fullAccess') }}
155
188
  </span>
156
189
  <span v-else class="text-sm">
157
- {{ t('supabase.access.roles.permissionCount', row.original.capabilities.length) }}
190
+ {{
191
+ t(
192
+ 'supabase.access.roles.permissionCount',
193
+ row.original.capabilities.length
194
+ )
195
+ }}
158
196
  </span>
159
197
  </template>
160
198
 
161
199
  <template #users-cell="{ row }">
162
- <span class="text-sm">{{ t('supabase.access.roles.userCount', row.original.user_count) }}</span>
200
+ <span class="text-sm">{{
201
+ t('supabase.access.roles.userCount', row.original.user_count)
202
+ }}</span>
163
203
  </template>
164
204
 
165
205
  <template #actions-cell="{ row }">
166
206
  <div class="flex justify-end">
167
- <UDropdownMenu :items="menuItems(row.original)" :content="{ align: 'end' }">
207
+ <UDropdownMenu
208
+ :items="menuItems(row.original)"
209
+ :content="{ align: 'end' }"
210
+ >
168
211
  <UButton
169
212
  :aria-label="row.original.label"
170
213
  icon="lucide:ellipsis"
@@ -17,10 +17,18 @@ import {
17
17
  } from '../../../utils/access-ui'
18
18
 
19
19
  // The modals and the slideover load on demand. They stay out of the page chunk.
20
- const InviteModal = defineAsyncComponent(() => import('../../../components/access/InviteModal.vue'))
21
- const EditRolesSlideover = defineAsyncComponent(() => import('../../../components/access/EditRolesSlideover.vue'))
22
- const RevokeModal = defineAsyncComponent(() => import('../../../components/access/RevokeModal.vue'))
23
- const LinkModal = defineAsyncComponent(() => import('../../../components/access/LinkModal.vue'))
20
+ const InviteModal = defineAsyncComponent(
21
+ () => import('../../../components/access/InviteModal.vue')
22
+ )
23
+ const EditRolesSlideover = defineAsyncComponent(
24
+ () => import('../../../components/access/EditRolesSlideover.vue')
25
+ )
26
+ const RevokeModal = defineAsyncComponent(
27
+ () => import('../../../components/access/RevokeModal.vue')
28
+ )
29
+ const LinkModal = defineAsyncComponent(
30
+ () => import('../../../components/access/LinkModal.vue')
31
+ )
24
32
 
25
33
  const { t, d, locale } = usePlutoI18n()
26
34
  const toast = useToast()
@@ -30,7 +38,8 @@ const permissions = usePlutoPermissions()
30
38
  const auth = usePlutoAuth()
31
39
 
32
40
  useHead({
33
- title: () => `${t('supabase.access.users.title')} - ${t('pluto.pages.settings')}`,
41
+ title: () =>
42
+ `${t('supabase.access.users.title')} - ${t('pluto.pages.settings')}`,
34
43
  })
35
44
 
36
45
  const users = ref<AccessUser[]>([])
@@ -65,7 +74,9 @@ async function load() {
65
74
 
66
75
  async function loadCapabilities() {
67
76
  try {
68
- capabilities.value = await $fetch<AccessCapabilities>('/api/_pluto/supabase/access/capabilities')
77
+ capabilities.value = await $fetch<AccessCapabilities>(
78
+ '/api/_pluto/supabase/access/capabilities'
79
+ )
69
80
  } catch {
70
81
  // The invite works without it. Only the "Copy invite link" action hides.
71
82
  capabilities.value = null
@@ -75,11 +86,15 @@ async function loadCapabilities() {
75
86
  onMounted(load)
76
87
 
77
88
  // Only an admin can read this, so ask after the permissions are known.
78
- watch(isAdmin, (admin) => {
79
- if (admin && !capabilities.value) {
80
- void loadCapabilities()
81
- }
82
- }, { immediate: true })
89
+ watch(
90
+ isAdmin,
91
+ (admin) => {
92
+ if (admin && !capabilities.value) {
93
+ void loadCapabilities()
94
+ }
95
+ },
96
+ { immediate: true }
97
+ )
83
98
 
84
99
  const columns = computed<TableColumn<AccessUser>[]>(() => [
85
100
  { id: 'user', header: t('supabase.access.users.columns.user') },
@@ -89,7 +104,10 @@ const columns = computed<TableColumn<AccessUser>[]>(() => [
89
104
  { id: 'actions', header: '' },
90
105
  ])
91
106
 
92
- function dateText(value: string | null, options: Intl.DateTimeFormatOptions): string {
107
+ function dateText(
108
+ value: string | null,
109
+ options: Intl.DateTimeFormatOptions
110
+ ): string {
93
111
  return value ? d(new Date(value), options) : ''
94
112
  }
95
113
 
@@ -134,10 +152,13 @@ async function copyLink(user: AccessUser) {
134
152
  }
135
153
 
136
154
  try {
137
- const result = await $fetch<{ link: string | null }>('/api/_pluto/supabase/access/users/invite-link', {
138
- method: 'POST',
139
- body: { email: user.email, roleKeys: user.roles },
140
- })
155
+ const result = await $fetch<{ link: string | null }>(
156
+ '/api/_pluto/supabase/access/users/invite-link',
157
+ {
158
+ method: 'POST',
159
+ body: { email: user.email, roleKeys: user.roles },
160
+ }
161
+ )
141
162
 
142
163
  // A confirmed account gets no link: it signs in with its own password.
143
164
  if (!result.link) {
@@ -163,7 +184,7 @@ async function copyLink(user: AccessUser) {
163
184
  // --- Resend ---
164
185
  async function resend(user: AccessUser) {
165
186
  try {
166
- const result = await $fetch<{ emailSent?: boolean, link?: string }>(
187
+ const result = await $fetch<{ emailSent?: boolean; link?: string }>(
167
188
  `/api/_pluto/supabase/access/users/${user.id}/resend`,
168
189
  { method: 'POST', body: { locale: locale.value } }
169
190
  )
@@ -238,8 +259,12 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
238
259
  <div class="flex flex-col gap-y-6">
239
260
  <div class="flex flex-wrap items-start justify-between gap-4">
240
261
  <div>
241
- <h2 class="text-xl font-semibold">{{ t('supabase.access.users.title') }}</h2>
242
- <p class="text-muted text-sm">{{ t('supabase.access.users.description') }}</p>
262
+ <h2 class="text-xl font-semibold">
263
+ {{ t('supabase.access.users.title') }}
264
+ </h2>
265
+ <p class="text-muted text-sm">
266
+ {{ t('supabase.access.users.description') }}
267
+ </p>
243
268
  </div>
244
269
 
245
270
  <UButton v-if="isAdmin" icon="lucide:user-plus" @click="openInvite">
@@ -248,7 +273,11 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
248
273
  </div>
249
274
 
250
275
  <!-- Loading -->
251
- <div v-if="status === 'loading'" class="flex flex-col gap-y-3" aria-busy="true">
276
+ <div
277
+ v-if="status === 'loading'"
278
+ class="flex flex-col gap-y-3"
279
+ aria-busy="true"
280
+ >
252
281
  <USkeleton v-for="row in 4" :key="row" class="h-12 w-full" />
253
282
  </div>
254
283
 
@@ -257,7 +286,14 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
257
286
  v-else-if="status === 'error'"
258
287
  :title="t('supabase.access.users.error.title')"
259
288
  :description="loadError"
260
- :actions="[{ label: t('pluto.common.tryAgain'), color: 'neutral', variant: 'outline', onClick: load }]"
289
+ :actions="[
290
+ {
291
+ label: t('pluto.common.tryAgain'),
292
+ color: 'neutral',
293
+ variant: 'outline',
294
+ onClick: load,
295
+ },
296
+ ]"
261
297
  color="error"
262
298
  variant="subtle"
263
299
  icon="lucide:circle-x"
@@ -268,16 +304,33 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
268
304
  v-else-if="users.length === 0"
269
305
  :title="t('supabase.access.users.empty.title')"
270
306
  :description="t('supabase.access.users.empty.description')"
271
- :actions="isAdmin ? [{ label: t('supabase.access.users.invite'), icon: 'lucide:user-plus', onClick: openInvite }] : []"
307
+ :actions="
308
+ isAdmin
309
+ ? [
310
+ {
311
+ label: t('supabase.access.users.invite'),
312
+ icon: 'lucide:user-plus',
313
+ onClick: openInvite,
314
+ },
315
+ ]
316
+ : []
317
+ "
272
318
  icon="lucide:users"
273
319
  />
274
320
 
275
321
  <!-- The list -->
276
- <UTable v-else :data="users" :columns="columns" class="border-default rounded-lg border">
322
+ <UTable
323
+ v-else
324
+ :data="users"
325
+ :columns="columns"
326
+ class="border-default rounded-lg border"
327
+ >
277
328
  <template #user-cell="{ row }">
278
329
  <div class="flex min-w-0 flex-col">
279
330
  <span class="flex items-center gap-2">
280
- <span class="text-highlighted truncate font-medium">{{ userDisplayName(row.original) }}</span>
331
+ <span class="text-highlighted truncate font-medium">{{
332
+ userDisplayName(row.original)
333
+ }}</span>
281
334
  <UBadge
282
335
  v-if="row.original.id === selfId"
283
336
  :label="t('supabase.access.users.you')"
@@ -286,7 +339,9 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
286
339
  size="sm"
287
340
  />
288
341
  </span>
289
- <span v-if="row.original.email" class="text-muted truncate text-sm">{{ row.original.email }}</span>
342
+ <span v-if="row.original.email" class="text-muted truncate text-sm">{{
343
+ row.original.email
344
+ }}</span>
290
345
  </div>
291
346
  </template>
292
347
 
@@ -325,7 +380,13 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
325
380
  variant="subtle"
326
381
  />
327
382
  <span v-if="row.original.invited_at" class="text-muted text-xs">
328
- {{ t('supabase.access.users.status.invitedOn', { date: dateText(row.original.invited_at, { dateStyle: 'medium' }) }) }}
383
+ {{
384
+ t('supabase.access.users.status.invitedOn', {
385
+ date: dateText(row.original.invited_at, {
386
+ dateStyle: 'medium',
387
+ }),
388
+ })
389
+ }}
329
390
  </span>
330
391
  </template>
331
392
  </div>
@@ -333,16 +394,30 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
333
394
 
334
395
  <template #lastSignIn-cell="{ row }">
335
396
  <span v-if="row.original.last_sign_in_at" class="text-sm">
336
- {{ dateText(row.original.last_sign_in_at, { dateStyle: 'medium', timeStyle: 'short' }) }}
397
+ {{
398
+ dateText(row.original.last_sign_in_at, {
399
+ dateStyle: 'medium',
400
+ timeStyle: 'short',
401
+ })
402
+ }}
337
403
  </span>
338
- <span v-else class="text-muted text-sm">{{ t('supabase.access.users.neverSignedIn') }}</span>
404
+ <span v-else class="text-muted text-sm">{{
405
+ t('supabase.access.users.neverSignedIn')
406
+ }}</span>
339
407
  </template>
340
408
 
341
409
  <template #actions-cell="{ row }">
342
410
  <div v-if="isAdmin" class="flex justify-end">
343
- <UDropdownMenu :items="menuItems(row.original)" :content="{ align: 'end' }">
411
+ <UDropdownMenu
412
+ :items="menuItems(row.original)"
413
+ :content="{ align: 'end' }"
414
+ >
344
415
  <UButton
345
- :aria-label="t('supabase.access.users.actionsLabel', { name: userDisplayName(row.original) })"
416
+ :aria-label="
417
+ t('supabase.access.users.actionsLabel', {
418
+ name: userDisplayName(row.original),
419
+ })
420
+ "
346
421
  icon="lucide:ellipsis"
347
422
  color="neutral"
348
423
  variant="ghost"
@@ -225,7 +225,10 @@ function handleStepChange(step: number) {
225
225
  class="flex flex-col gap-y-4"
226
226
  @submit="handleStepChange(currentStep)"
227
227
  >
228
- <UFormField :label="t('supabase.connection.connectionString')" required>
228
+ <UFormField
229
+ :label="t('supabase.connection.connectionString')"
230
+ required
231
+ >
229
232
  <UInput
230
233
  v-model="databaseForm.connectionString"
231
234
  :disabled="isSettingUpDatabase"
@@ -245,7 +248,9 @@ function handleStepChange(step: number) {
245
248
  class="underline"
246
249
  external
247
250
  >
248
- {{ t('supabase.connection.connectionStringHelpLink') }}
251
+ {{
252
+ t('supabase.connection.connectionStringHelpLink')
253
+ }}
249
254
  </ULink>
250
255
  </template>
251
256
  </SupabaseMessageParts>
@@ -296,7 +301,9 @@ function handleStepChange(step: number) {
296
301
  :message="t('supabase.setup.why')"
297
302
  >
298
303
  <template #promise>
299
- <UTooltip :text="t('supabase.setup.promiseTooltip')">
304
+ <UTooltip
305
+ :text="t('supabase.setup.promiseTooltip')"
306
+ >
300
307
  <span
301
308
  class="underline decoration-dotted cursor-help"
302
309
  >
@@ -324,7 +331,9 @@ function handleStepChange(step: number) {
324
331
  >
325
332
  <p>
326
333
  <SupabaseMessageParts
327
- :message="t('supabase.setup.technicalDetailsRequest')"
334
+ :message="
335
+ t('supabase.setup.technicalDetailsRequest')
336
+ "
328
337
  >
329
338
  <template #route>
330
339
  <ULink
@@ -332,7 +341,11 @@ function handleStepChange(step: number) {
332
341
  target="_blank"
333
342
  class="underline"
334
343
  >
335
- {{ t('supabase.setup.technicalDetailsRoute') }}</ULink
344
+ {{
345
+ t(
346
+ 'supabase.setup.technicalDetailsRoute'
347
+ )
348
+ }}</ULink
336
349
  >
337
350
  </template>
338
351
  <template #files>
@@ -341,7 +354,11 @@ function handleStepChange(step: number) {
341
354
  target="_blank"
342
355
  class="underline"
343
356
  >
344
- {{ t('supabase.setup.technicalDetailsFiles') }}</ULink
357
+ {{
358
+ t(
359
+ 'supabase.setup.technicalDetailsFiles'
360
+ )
361
+ }}</ULink
345
362
  >
346
363
  </template>
347
364
  </SupabaseMessageParts>
@@ -374,7 +391,9 @@ function handleStepChange(step: number) {
374
391
  <hgroup class="text-center">
375
392
  <span class="text-3xl">🎉</span>
376
393
 
377
- <h1 class="font-bold text-2xl">{{ t('supabase.setup.doneTitle') }}</h1>
394
+ <h1 class="font-bold text-2xl">
395
+ {{ t('supabase.setup.doneTitle') }}
396
+ </h1>
378
397
  </hgroup>
379
398
 
380
399
  <p class="text-balance text-center text-lg">