@plutocms/supabase 0.30.0 → 0.31.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.
@@ -266,183 +266,185 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
266
266
  </script>
267
267
 
268
268
  <template>
269
- <main>
270
- <AdminView>
271
- <!-- The section (the Users item of the menu) and its tabs, like Settings. -->
272
- <h1 class="text-3xl font-bold lg:text-4xl">
273
- {{ t('supabase.access.users.title') }}
274
- </h1>
275
-
276
- <PlutoAdminNavTabs />
277
-
278
- <div class="flex flex-wrap items-start justify-between gap-4">
279
- <div>
280
- <h2 class="text-xl font-semibold">
281
- {{ t('supabase.access.users.title') }}
282
- </h2>
283
- <p class="text-muted text-sm">
284
- {{ t('supabase.access.users.description') }}
285
- </p>
286
- </div>
287
-
288
- <UButton v-if="isAdmin" icon="lucide:user-plus" @click="openInvite">
289
- {{ t('supabase.access.users.invite') }}
290
- </UButton>
291
- </div>
292
-
293
- <UAlert
294
- v-if="!allowed"
295
- :title="t('pluto.settings.noPermission.title')"
296
- :description="
297
- t('pluto.settings.noPermission.description', {
298
- page: t('supabase.access.users.title'),
299
- })
300
- "
301
- icon="lucide:lock"
302
- color="warning"
303
- />
304
-
305
- <!-- Loading -->
306
- <div
307
- v-else-if="status === 'loading'"
308
- class="flex flex-col gap-y-3"
309
- aria-busy="true"
310
- >
311
- <USkeleton v-for="row in 4" :key="row" class="h-12 w-full" />
312
- </div>
313
-
314
- <!-- Error -->
315
- <UAlert
316
- v-else-if="status === 'error'"
317
- :title="t('supabase.access.users.error.title')"
318
- :description="loadError"
319
- :actions="[
320
- {
321
- label: t('pluto.common.tryAgain'),
322
- color: 'neutral',
323
- variant: 'outline',
324
- onClick: load,
325
- },
326
- ]"
327
- color="error"
328
- variant="subtle"
329
- icon="lucide:circle-x"
330
- />
331
-
332
- <!-- Empty -->
333
- <UEmpty
334
- v-else-if="users.length === 0"
335
- :title="t('supabase.access.users.empty.title')"
336
- :description="t('supabase.access.users.empty.description')"
337
- :actions="
338
- isAdmin
339
- ? [
340
- {
341
- label: t('supabase.access.users.invite'),
342
- icon: 'lucide:user-plus',
343
- onClick: openInvite,
344
- },
345
- ]
346
- : []
347
- "
269
+ <PlutoViewAdminPanel>
270
+ <!-- The section (the Users item of the menu) and its tabs, like Settings. -->
271
+ <template #toolbar>
272
+ <PlutoViewToolbar
273
+ :title="t('supabase.access.users.title')"
348
274
  icon="lucide:users"
349
- />
350
-
351
- <!-- The list -->
352
- <UTable
353
- v-else
354
- :data="users"
355
- :columns="columns"
356
- class="border-default rounded-lg border"
357
275
  >
358
- <template #user-cell="{ row }">
359
- <div class="flex min-w-0 flex-col">
360
- <span class="flex items-center gap-2">
361
- <span class="text-highlighted truncate font-medium">{{
362
- userDisplayName(row.original)
363
- }}</span>
364
- <UBadge
365
- v-if="row.original.id === selfId"
366
- :label="t('supabase.access.users.you')"
367
- color="primary"
368
- variant="subtle"
369
- size="sm"
370
- />
371
- </span>
372
- <span
373
- v-if="row.original.email"
374
- class="text-muted truncate text-sm"
375
- >{{ row.original.email }}</span
376
- >
377
- </div>
276
+ <template #right>
277
+ <UButton v-if="isAdmin" icon="lucide:user-plus" @click="openInvite">
278
+ {{ t('supabase.access.users.invite') }}
279
+ </UButton>
378
280
  </template>
281
+ </PlutoViewToolbar>
379
282
 
380
- <template #roles-cell="{ row }">
381
- <div class="flex flex-wrap gap-1">
283
+ <PlutoAdminNavTabs />
284
+ </template>
285
+
286
+ <p class="text-muted text-sm">
287
+ {{ t('supabase.access.users.description') }}
288
+ </p>
289
+
290
+ <UAlert
291
+ v-if="!allowed"
292
+ :title="t('pluto.settings.noPermission.title')"
293
+ :description="
294
+ t('pluto.settings.noPermission.description', {
295
+ page: t('supabase.access.users.title'),
296
+ })
297
+ "
298
+ icon="lucide:lock"
299
+ color="warning"
300
+ />
301
+
302
+ <!-- Loading -->
303
+ <div
304
+ v-else-if="status === 'loading'"
305
+ class="flex flex-col gap-y-3"
306
+ aria-busy="true"
307
+ >
308
+ <USkeleton v-for="row in 4" :key="row" class="h-12 w-full" />
309
+ </div>
310
+
311
+ <!-- Error -->
312
+ <UAlert
313
+ v-else-if="status === 'error'"
314
+ :title="t('supabase.access.users.error.title')"
315
+ :description="loadError"
316
+ :actions="[
317
+ {
318
+ label: t('pluto.common.tryAgain'),
319
+ color: 'neutral',
320
+ variant: 'outline',
321
+ onClick: load,
322
+ },
323
+ ]"
324
+ color="error"
325
+ variant="subtle"
326
+ icon="lucide:circle-x"
327
+ />
328
+
329
+ <!-- Empty -->
330
+ <UEmpty
331
+ v-else-if="users.length === 0"
332
+ :title="t('supabase.access.users.empty.title')"
333
+ :description="t('supabase.access.users.empty.description')"
334
+ :actions="
335
+ isAdmin
336
+ ? [
337
+ {
338
+ label: t('supabase.access.users.invite'),
339
+ icon: 'lucide:user-plus',
340
+ onClick: openInvite,
341
+ },
342
+ ]
343
+ : []
344
+ "
345
+ icon="lucide:users"
346
+ />
347
+
348
+ <!-- The list -->
349
+ <UTable
350
+ v-else
351
+ :data="users"
352
+ :columns="columns"
353
+ class="border-default rounded-lg border"
354
+ >
355
+ <template #user-cell="{ row }">
356
+ <div class="flex min-w-0 flex-col">
357
+ <span class="flex items-center gap-2">
358
+ <span class="text-highlighted truncate font-medium">{{
359
+ userDisplayName(row.original)
360
+ }}</span>
382
361
  <UBadge
383
- v-if="isAdminUser(row.original)"
384
- :label="t('supabase.access.users.administrator')"
362
+ v-if="row.original.id === selfId"
363
+ :label="t('supabase.access.users.you')"
385
364
  color="primary"
386
365
  variant="subtle"
366
+ size="sm"
387
367
  />
388
- <template v-else>
389
- <UBadge
390
- v-for="key in row.original.roles"
391
- :key="key"
392
- :label="roleLabelOf(roles, key)"
393
- color="neutral"
394
- variant="subtle"
395
- />
396
- </template>
397
- </div>
398
- </template>
399
-
400
- <template #status-cell="{ row }">
401
- <div class="flex flex-col items-start gap-1">
368
+ </span>
369
+ <span v-if="row.original.email" class="text-muted truncate text-sm">{{
370
+ row.original.email
371
+ }}</span>
372
+ </div>
373
+ </template>
374
+
375
+ <template #roles-cell="{ row }">
376
+ <div class="flex flex-wrap gap-1">
377
+ <UBadge
378
+ v-if="isAdminUser(row.original)"
379
+ :label="t('supabase.access.users.administrator')"
380
+ color="primary"
381
+ variant="subtle"
382
+ />
383
+ <template v-else>
402
384
  <UBadge
403
- v-if="userStatus(row.original) === 'active'"
404
- :label="t('supabase.access.users.status.active')"
405
- color="success"
385
+ v-for="key in row.original.roles"
386
+ :key="key"
387
+ :label="roleLabelOf(roles, key)"
388
+ color="neutral"
406
389
  variant="subtle"
407
390
  />
408
- <template v-else>
409
- <UBadge
410
- :label="t('supabase.access.users.status.invited')"
411
- color="warning"
412
- variant="subtle"
413
- />
414
- <span v-if="row.original.invited_at" class="text-muted text-xs">
415
- {{
416
- t('supabase.access.users.status.invitedOn', {
417
- date: dateText(row.original.invited_at, {
418
- dateStyle: 'medium',
419
- }),
420
- })
421
- }}
422
- </span>
423
- </template>
424
- </div>
425
- </template>
426
-
427
- <template #lastSignIn-cell="{ row }">
428
- <span v-if="row.original.last_sign_in_at" class="text-sm">
429
- {{
430
- dateText(row.original.last_sign_in_at, {
431
- dateStyle: 'medium',
432
- timeStyle: 'short',
433
- })
434
- }}
435
- </span>
436
- <span v-else class="text-muted text-sm">{{
437
- t('supabase.access.users.neverSignedIn')
438
- }}</span>
439
- </template>
440
-
441
- <template #actions-cell="{ row }">
442
- <div v-if="isAdmin" class="flex justify-end">
443
- <UDropdownMenu
444
- :items="menuItems(row.original)"
445
- :content="{ align: 'end' }"
391
+ </template>
392
+ </div>
393
+ </template>
394
+
395
+ <template #status-cell="{ row }">
396
+ <div class="flex flex-col items-start gap-1">
397
+ <UBadge
398
+ v-if="userStatus(row.original) === 'active'"
399
+ :label="t('supabase.access.users.status.active')"
400
+ color="success"
401
+ variant="subtle"
402
+ />
403
+ <template v-else>
404
+ <UBadge
405
+ :label="t('supabase.access.users.status.invited')"
406
+ color="warning"
407
+ variant="subtle"
408
+ />
409
+ <span v-if="row.original.invited_at" class="text-muted text-xs">
410
+ {{
411
+ t('supabase.access.users.status.invitedOn', {
412
+ date: dateText(row.original.invited_at, {
413
+ dateStyle: 'medium',
414
+ }),
415
+ })
416
+ }}
417
+ </span>
418
+ </template>
419
+ </div>
420
+ </template>
421
+
422
+ <template #lastSignIn-cell="{ row }">
423
+ <span v-if="row.original.last_sign_in_at" class="text-sm">
424
+ {{
425
+ dateText(row.original.last_sign_in_at, {
426
+ dateStyle: 'medium',
427
+ timeStyle: 'short',
428
+ })
429
+ }}
430
+ </span>
431
+ <span v-else class="text-muted text-sm">{{
432
+ t('supabase.access.users.neverSignedIn')
433
+ }}</span>
434
+ </template>
435
+
436
+ <template #actions-cell="{ row }">
437
+ <div v-if="isAdmin" class="flex justify-end">
438
+ <UDropdownMenu
439
+ :items="menuItems(row.original)"
440
+ :content="{ align: 'end' }"
441
+ >
442
+ <UTooltip
443
+ :text="
444
+ t('supabase.access.users.actionsLabel', {
445
+ name: userDisplayName(row.original),
446
+ })
447
+ "
446
448
  >
447
449
  <UButton
448
450
  :aria-label="
@@ -454,27 +456,27 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
454
456
  color="neutral"
455
457
  variant="ghost"
456
458
  />
457
- </UDropdownMenu>
458
- </div>
459
- </template>
460
- </UTable>
461
-
462
- <!-- Lazy: the chunk loads on the first open. -->
463
- <InviteModal
464
- v-if="inviteMounted"
465
- v-model:open="inviteOpen"
466
- :roles="roles"
467
- :capabilities="capabilities"
468
- @done="load"
469
- />
470
- <EditRolesSlideover
471
- v-if="editMounted"
472
- v-model:open="editOpen"
473
- :user="editUser"
474
- :roles="roles"
475
- :self="editUser?.id === selfId"
476
- @saved="load"
477
- />
478
- </AdminView>
479
- </main>
459
+ </UTooltip>
460
+ </UDropdownMenu>
461
+ </div>
462
+ </template>
463
+ </UTable>
464
+
465
+ <!-- Lazy: the chunk loads on the first open. -->
466
+ <InviteModal
467
+ v-if="inviteMounted"
468
+ v-model:open="inviteOpen"
469
+ :roles="roles"
470
+ :capabilities="capabilities"
471
+ @done="load"
472
+ />
473
+ <EditRolesSlideover
474
+ v-if="editMounted"
475
+ v-model:open="editOpen"
476
+ :user="editUser"
477
+ :roles="roles"
478
+ :self="editUser?.id === selfId"
479
+ @saved="load"
480
+ />
481
+ </PlutoViewAdminPanel>
480
482
  </template>
@@ -146,22 +146,20 @@
146
146
  "checkConsole": "Please check the console for more details."
147
147
  },
148
148
  "setup": {
149
- "title": "Setup Wizard",
150
- "steps": {
151
- "database": "Database Setup",
152
- "complete": "Complete Setup"
153
- },
154
- "submit": "Set up database",
155
- "next": "Submit",
156
- "why": "It will be used to create your database structure. It won't be stored anywhere. That's a {promise}! 😉",
157
- "promise": "promise",
158
- "promiseTooltip": "Click on the 'Technical details' button for more info",
159
- "technicalDetails": "Technical details",
160
- "technicalDetailsRequest": "When you click \"Next\", a request to {route} will be made with the database password you provided. This API will then use this password to make a direct PostgreSQL connection to your Supabase database and run the SQL queries from this {files}.",
161
- "technicalDetailsRoute": "a server route",
162
- "technicalDetailsFiles": "SQL files",
163
- "technicalDetailsStored": "After the setup is complete, the password will NOT be stored or used for any other purpose.",
164
- "restartDescription": "Saving your connection string writes it to .env. In development, that restarts the server automatically. The page will briefly show a reconnecting message — this is expected, not an error.",
149
+ "title": "Set up the database",
150
+ "description": "Pluto creates its tables in your Supabase project. Paste the connection details of the database.",
151
+ "connectionHelp": "Copy the URI of the Transaction pooler from the {link} of your project.",
152
+ "connectionHelpLink": "Connect panel",
153
+ "passwordHelp": "It replaces {placeholder} in the URI. You can reset it in the {link}.",
154
+ "passwordHelpLink": "database settings",
155
+ "stored": "The connection stays on the server: it is saved in `.env` as `DATABASE_URL`, for the next updates of the tables. It is never sent back to the browser.",
156
+ "howItWorks": "What happens",
157
+ "detailsRequest": "The button sends the connection to a {route} of this site.",
158
+ "detailsRoute": "server route",
159
+ "detailsRun": "The server connects to your database and runs the {files} of Pluto and of its layers.",
160
+ "detailsFiles": "SQL migrations",
161
+ "restartNote": "In development, saving `.env` restarts the server. The page waits and reconnects.",
162
+ "submit": "Set up the database",
165
163
  "restartingDescription": "Setup is finishing. Reconnecting…",
166
164
  "reconnectedDescription": "The server is back. Your database has been set up.",
167
165
  "failed": "Database setup failed",
@@ -169,9 +167,9 @@
169
167
  "complete": "Database setup complete",
170
168
  "completeDescription": "Your database has been set up successfully.",
171
169
  "error": "Error setting up the database",
172
- "doneTitle": "Setup Complete",
173
- "doneDescription": "Your database is ready. Create your admin account by signing up.",
174
- "goToSignup": "Go to sign up"
170
+ "doneTitle": "The database is ready",
171
+ "doneDescription": "Now create your administrator account.",
172
+ "goToSignup": "Create the account"
175
173
  },
176
174
  "migrations": {
177
175
  "title": "Migrations",
@@ -146,22 +146,20 @@
146
146
  "checkConsole": "Verifique o console para ver mais detalhes."
147
147
  },
148
148
  "setup": {
149
- "title": "Assistente de configuração",
150
- "steps": {
151
- "database": "Configuração do banco de dados",
152
- "complete": "Concluir configuração"
153
- },
154
- "submit": "Configurar banco de dados",
155
- "next": "Enviar",
156
- "why": "Ela será usada para criar a estrutura do seu banco de dados. Ela não será armazenada em nenhum lugar. Isso é uma {promise}! 😉",
157
- "promise": "promessa",
158
- "promiseTooltip": "Clique no botão 'Detalhes técnicos' para saber mais",
159
- "technicalDetails": "Detalhes técnicos",
160
- "technicalDetailsRequest": "Ao clicar em \"Próximo\", será feita uma requisição para {route} com a senha do banco de dados que você informou. Essa API usará a senha para abrir uma conexão direta do PostgreSQL com seu banco de dados do Supabase e executar as consultas SQL destes {files}.",
161
- "technicalDetailsRoute": "uma rota do servidor",
162
- "technicalDetailsFiles": "arquivos SQL",
163
- "technicalDetailsStored": "Depois que a configuração terminar, a senha NÃO será armazenada nem usada para nenhum outro fim.",
164
- "restartDescription": "Salvar sua string de conexão a grava no arquivo .env. Em desenvolvimento, isso reinicia o servidor automaticamente. A página vai exibir brevemente uma mensagem de reconexão. Isso é esperado e não é um erro.",
149
+ "title": "Configure o banco de dados",
150
+ "description": "O Pluto cria as tabelas dele no seu projeto do Supabase. Cole os dados de conexão do banco de dados.",
151
+ "connectionHelp": "Copie a URI do Transaction pooler no {link} do seu projeto.",
152
+ "connectionHelpLink": "painel Connect",
153
+ "passwordHelp": "Ela substitui {placeholder} na URI. Você pode redefini-la nas {link}.",
154
+ "passwordHelpLink": "configurações do banco de dados",
155
+ "stored": "A conexão fica no servidor: ela é salva no `.env` como `DATABASE_URL`, para as próximas atualizações das tabelas. Ela nunca volta para o navegador.",
156
+ "howItWorks": "O que acontece",
157
+ "detailsRequest": "O botão envia a conexão para uma {route} deste site.",
158
+ "detailsRoute": "rota do servidor",
159
+ "detailsRun": "O servidor se conecta ao seu banco de dados e roda as {files} do Pluto e das camadas dele.",
160
+ "detailsFiles": "migrações SQL",
161
+ "restartNote": "Em desenvolvimento, salvar o `.env` reinicia o servidor. A página espera e se reconecta.",
162
+ "submit": "Configurar o banco de dados",
165
163
  "restartingDescription": "A configuração está terminando. Reconectando…",
166
164
  "reconnectedDescription": "O servidor voltou. Seu banco de dados foi configurado.",
167
165
  "failed": "Falha ao configurar o banco de dados",
@@ -169,9 +167,9 @@
169
167
  "complete": "Banco de dados configurado",
170
168
  "completeDescription": "Seu banco de dados foi configurado com sucesso.",
171
169
  "error": "Erro ao configurar o banco de dados",
172
- "doneTitle": "Configuração concluída",
173
- "doneDescription": "Seu banco de dados está pronto. Crie sua conta de administrador fazendo o cadastro.",
174
- "goToSignup": "Ir para o cadastro"
170
+ "doneTitle": "O banco de dados está pronto",
171
+ "doneDescription": "Agora crie sua conta de administrador.",
172
+ "goToSignup": "Criar a conta"
175
173
  },
176
174
  "migrations": {
177
175
  "title": "Migrações",
@@ -0,0 +1,27 @@
1
+ // Not a Nuxt module: Nuxt loads every file of modules/ (and modules/*/index.ts), not this folder.
2
+ /**
3
+ * A shim that pluto-migrations wrote: one re-export of the types of an @plutocms/supabase copy
4
+ * (installed, or linked from a local checkout).
5
+ */
6
+ const SEEDED_SHIM = /^export \* from '[^']*\/shared\/types\/supabase'\s*$/
7
+
8
+ /**
9
+ * The text to write to the app's `shared/types/supabase.ts`, or `null` to leave the file as it
10
+ * is. A missing file gets the shim. A shim that pluto-migrations wrote earlier gets the new path
11
+ * when the path changed: a package manager with versioned folders (Bun's
12
+ * `node_modules/.bun/@plutocms+supabase@<version>...`) moves the package on each update, and the
13
+ * old path then has no types. A file with other text (types that the app generated) stays.
14
+ */
15
+ export function supabaseTypesShim(
16
+ existing: string | undefined,
17
+ importSpecifier: string
18
+ ): string | null {
19
+ const shim = `export * from '${importSpecifier}'\n`
20
+ if (existing === undefined) {
21
+ return shim
22
+ }
23
+ if (SEEDED_SHIM.test(existing) && existing.trim() !== shim.trim()) {
24
+ return shim
25
+ }
26
+ return null
27
+ }
@@ -9,6 +9,7 @@ import {
9
9
  } from 'node:fs'
10
10
  import { basename, dirname, join, relative } from 'node:path'
11
11
  import { createResolver, defineNuxtModule, getLayerDirectories } from 'nuxt/kit'
12
+ import { supabaseTypesShim } from './internal/supabase-types-shim'
12
13
 
13
14
  const noTransactionPattern = /^--\s*pluto:no-transaction\b/
14
15
 
@@ -116,7 +117,9 @@ export default defineNuxtModule({
116
117
  nuxt.options.rootDir,
117
118
  'shared/types/supabase.ts'
118
119
  )
119
- if (!existsSync(consumerTypesPath)) {
120
+ // Seed the shim when the app has none, and fix the path of an earlier
121
+ // shim (see supabaseTypesShim).
122
+ {
120
123
  // Resolve *this exact* @plutocms/supabase instance's own snapshot
121
124
  // (local-linked or npm-installed, whichever is actually running)
122
125
  // rather than a bare package specifier, which would re-resolve
@@ -132,15 +135,19 @@ export default defineNuxtModule({
132
135
  importSpecifier = `./${importSpecifier}`
133
136
  }
134
137
 
135
- mkdirSync(consumerTypesDir, { recursive: true })
136
- writeFileSync(
137
- consumerTypesPath,
138
- `export * from '${importSpecifier}'\n`,
139
- 'utf-8'
140
- )
141
- console.warn(
142
- '[pluto-migrations] Seeded shared/types/supabase.ts (re-exporting @plutocms/supabase types; no generated types found yet).'
143
- )
138
+ const existing = existsSync(consumerTypesPath)
139
+ ? readFileSync(consumerTypesPath, 'utf-8')
140
+ : undefined
141
+ const shim = supabaseTypesShim(existing, importSpecifier)
142
+ if (shim !== null) {
143
+ mkdirSync(consumerTypesDir, { recursive: true })
144
+ writeFileSync(consumerTypesPath, shim, 'utf-8')
145
+ console.warn(
146
+ existing === undefined
147
+ ? '[pluto-migrations] Seeded shared/types/supabase.ts (re-exporting @plutocms/supabase types; no generated types found yet).'
148
+ : '[pluto-migrations] Updated the path in shared/types/supabase.ts (the @plutocms/supabase package moved).'
149
+ )
150
+ }
144
151
  }
145
152
 
146
153
  const layerDirs = getLayerDirectories()
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plutocms/supabase",
3
3
  "type": "module",
4
- "version": "0.30.0",
4
+ "version": "0.31.0",
5
5
  "description": "Supabase backend layer for Pluto",
6
6
  "author": "Victor Ribeiro",
7
7
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "dependencies": {
32
32
  "@nuxt/ui": "^4.12.0",
33
33
  "@nuxtjs/supabase": "^2.1.0",
34
- "@plutocms/core": ">=0.64.0 <1.0.0",
34
+ "@plutocms/core": ">=0.68.0 <1.0.0",
35
35
  "@supabase/supabase-js": "^2.117.3",
36
36
  "postgres": "^3.4.9",
37
37
  "supabase": "^2.121.0",
@@ -1,13 +0,0 @@
1
- <script setup lang="ts"></script>
2
-
3
- <template>
4
- <div
5
- class="dark:bg-admin-content light:bg-white font-outfit flex h-full grow flex-col"
6
- >
7
- <div class="flex h-full justify-center">
8
- <div class="grow overflow-hidden">
9
- <slot />
10
- </div>
11
- </div>
12
- </div>
13
- </template>