@plutocms/supabase 0.18.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.20.0](https://github.com/plutocms/supabase/compare/v0.19.0...v0.20.0) (2026-10-31)
4
+
5
+
6
+ ### Features
7
+
8
+ * section tabs on the Users and Roles pages ([#163](https://github.com/plutocms/supabase/issues/163)) ([876abf6](https://github.com/plutocms/supabase/commit/876abf637520f0059a29216a3cb6629980ad02e7))
9
+
10
+ ## [0.19.0](https://github.com/plutocms/supabase/compare/v0.18.1...v0.19.0) (2026-10-31)
11
+
12
+
13
+ ### Features
14
+
15
+ * a top-level Users menu with Users and Roles ([#158](https://github.com/plutocms/supabase/issues/158)) ([1fc3cdf](https://github.com/plutocms/supabase/commit/1fc3cdfa1859b8e0a512c71ec54bd64cc6cab79b))
16
+
3
17
  ## [0.18.1](https://github.com/plutocms/supabase/compare/v0.18.0...v0.18.1) (2026-10-31)
4
18
 
5
19
 
package/FEATURES.md CHANGED
@@ -11,7 +11,7 @@ policies shared by every project here.
11
11
  - Permissions storage: role-based capabilities backing server route guards and RLS policies, and
12
12
  the admin-only access API (invite, roles) of migration 008.
13
13
  @.claude/skills/permissions-storage/SKILL.md
14
- - Users and Roles: the Users and Roles tabs of Settings (invite, edit roles, revoke, copy invite
14
+ - Users and Roles: the Users item of the admin menu, with the Users and Roles pages (invite, edit roles, revoke, copy invite
15
15
  link, roles editor), invites through Pluto mail, and a guide for the Supabase sign-in emails.
16
16
  @.claude/skills/users-and-roles/SKILL.md
17
17
  - Content adapter: a Supabase/PostgREST-backed `PlutoContentAdapter`, plugging any layer's
package/README.md CHANGED
@@ -14,7 +14,7 @@ SUPABASE_URL=your-supabase-url
14
14
  SUPABASE_KEY=your-publishable-supabase-key
15
15
  ```
16
16
 
17
- 2. Install the layer. It brings `@nuxtjs/supabase`, `@plutocms/core` and `@plutocms/utils`, so you do not add them:
17
+ 2. Install the layer. It brings `@nuxtjs/supabase` and `@plutocms/core`, so you do not add them:
18
18
 
19
19
  ```bash
20
20
  npm i @plutocms/supabase
@@ -0,0 +1,263 @@
1
+ <script setup lang="ts">
2
+ // The Roles page (`/admin/roles`, admin only, a child of the Users item of the
3
+ // admin menu). A role is a set of capabilities. An admin creates, edits and
4
+ // deletes roles. The `admin` role is built in: it opens read-only, and it
5
+ // cannot be deleted. Without admin rights, the page shows an alert and loads
6
+ // nothing. See the users-and-roles skill.
7
+ import type { DropdownMenuItem, TableColumn } from '@nuxt/ui'
8
+ import type { AccessRole } from '../../../shared/types/access'
9
+ import { ADMIN_ROLE_KEY } from '../../utils/access-ui'
10
+
11
+ // The slideover and the modal load on demand. They stay out of the page chunk.
12
+ const RoleSlideover = defineAsyncComponent(
13
+ () => import('../../components/access/RoleSlideover.vue')
14
+ )
15
+ const DeleteRoleModal = defineAsyncComponent(
16
+ () => import('../../components/access/DeleteRoleModal.vue')
17
+ )
18
+
19
+ const { t } = usePlutoI18n()
20
+ const overlay = useOverlay()
21
+ const errorText = usePlutoErrorText()
22
+
23
+ useHead({
24
+ title: () => t('supabase.access.roles.title'),
25
+ })
26
+
27
+ const permissions = usePlutoPermissions()
28
+ const allowed = computed(() => permissions.isAdmin.value)
29
+
30
+ const roles = ref<AccessRole[]>([])
31
+ const status = ref<'loading' | 'ready' | 'error'>('loading')
32
+ const loadError = ref('')
33
+
34
+ async function load() {
35
+ // Keep the table on screen while it reloads after a change.
36
+ if (status.value !== 'ready') {
37
+ status.value = 'loading'
38
+ }
39
+
40
+ try {
41
+ const response = await $fetch<{ roles: AccessRole[] }>(
42
+ '/api/_pluto/supabase/access/roles'
43
+ )
44
+
45
+ roles.value = response.roles
46
+ status.value = 'ready'
47
+ } catch (error) {
48
+ loadError.value = errorText(error)
49
+ status.value = 'error'
50
+ }
51
+ }
52
+
53
+ watch(
54
+ allowed,
55
+ (ok) => {
56
+ if (ok) {
57
+ load()
58
+ }
59
+ },
60
+ { immediate: true }
61
+ )
62
+
63
+ const columns = computed<TableColumn<AccessRole>[]>(() => [
64
+ { id: 'role', header: t('supabase.access.roles.columns.role') },
65
+ { id: 'permissions', header: t('supabase.access.roles.columns.permissions') },
66
+ { id: 'users', header: t('supabase.access.roles.columns.users') },
67
+ { id: 'actions', header: '' },
68
+ ])
69
+
70
+ // --- Create and edit ---
71
+ const slideoverMounted = ref(false)
72
+ const slideoverOpen = ref(false)
73
+ const editing = ref<AccessRole | null>(null)
74
+
75
+ function openRole(role: AccessRole | null) {
76
+ editing.value = role
77
+ slideoverMounted.value = true
78
+ slideoverOpen.value = true
79
+ }
80
+
81
+ // --- Delete ---
82
+ async function openDelete(role: AccessRole) {
83
+ const modal = overlay.create(DeleteRoleModal)
84
+ const deleted = await modal.open({ role })
85
+
86
+ if (deleted) {
87
+ await load()
88
+ }
89
+ }
90
+
91
+ function isFullAccess(role: AccessRole): boolean {
92
+ return role.capabilities.includes('*')
93
+ }
94
+
95
+ function menuItems(role: AccessRole): DropdownMenuItem[][] {
96
+ const readOnly = role.key === ADMIN_ROLE_KEY
97
+ const main: DropdownMenuItem[] = [
98
+ {
99
+ label: readOnly
100
+ ? t('supabase.access.roles.menu.view')
101
+ : t('supabase.access.roles.menu.edit'),
102
+ icon: readOnly ? 'lucide:eye' : 'lucide:pencil',
103
+ onSelect: () => openRole(role),
104
+ },
105
+ ]
106
+
107
+ // A built-in role cannot be deleted.
108
+ if (role.is_builtin) {
109
+ return [main]
110
+ }
111
+
112
+ return [
113
+ main,
114
+ [
115
+ {
116
+ label: t('supabase.access.roles.menu.delete'),
117
+ icon: 'lucide:trash-2',
118
+ color: 'error',
119
+ onSelect: () => openDelete(role),
120
+ },
121
+ ],
122
+ ]
123
+ }
124
+ </script>
125
+
126
+ <template>
127
+ <main>
128
+ <AdminView>
129
+ <!-- The section (the Users item of the menu) and its tabs, like Settings. -->
130
+ <h1 class="text-3xl font-bold lg:text-4xl">
131
+ {{ t('supabase.access.users.title') }}
132
+ </h1>
133
+
134
+ <PlutoAdminNavTabs />
135
+
136
+ <div class="flex flex-wrap items-start justify-between gap-4">
137
+ <div>
138
+ <h2 class="text-xl font-semibold">
139
+ {{ t('supabase.access.roles.title') }}
140
+ </h2>
141
+ <p class="text-muted text-sm">
142
+ {{ t('supabase.access.roles.description') }}
143
+ </p>
144
+ </div>
145
+
146
+ <UButton icon="lucide:plus" @click="openRole(null)">
147
+ {{ t('supabase.access.roles.create') }}
148
+ </UButton>
149
+ </div>
150
+
151
+ <UAlert
152
+ v-if="!allowed"
153
+ :title="t('pluto.settings.noPermission.title')"
154
+ :description="
155
+ t('pluto.settings.noPermission.description', {
156
+ page: t('supabase.access.roles.title'),
157
+ })
158
+ "
159
+ icon="lucide:lock"
160
+ color="warning"
161
+ />
162
+
163
+ <!-- Loading -->
164
+ <div
165
+ v-else-if="status === 'loading'"
166
+ class="flex flex-col gap-y-3"
167
+ aria-busy="true"
168
+ >
169
+ <USkeleton v-for="row in 3" :key="row" class="h-12 w-full" />
170
+ </div>
171
+
172
+ <!-- Error -->
173
+ <UAlert
174
+ v-else-if="status === 'error'"
175
+ :title="t('supabase.access.roles.error.title')"
176
+ :description="loadError"
177
+ :actions="[
178
+ {
179
+ label: t('pluto.common.tryAgain'),
180
+ color: 'neutral',
181
+ variant: 'outline',
182
+ onClick: load,
183
+ },
184
+ ]"
185
+ color="error"
186
+ variant="subtle"
187
+ icon="lucide:circle-x"
188
+ />
189
+
190
+ <!-- The list -->
191
+ <UTable
192
+ v-else
193
+ :data="roles"
194
+ :columns="columns"
195
+ class="border-default rounded-lg border"
196
+ >
197
+ <template #role-cell="{ row }">
198
+ <div class="flex min-w-0 flex-col">
199
+ <span class="flex items-center gap-2">
200
+ <span class="text-highlighted truncate font-medium">{{
201
+ row.original.label
202
+ }}</span>
203
+ <UBadge
204
+ v-if="row.original.is_builtin"
205
+ :label="t('supabase.access.roles.builtin')"
206
+ color="neutral"
207
+ variant="subtle"
208
+ size="sm"
209
+ />
210
+ </span>
211
+ <code class="text-muted truncate font-mono text-xs">{{
212
+ row.original.key
213
+ }}</code>
214
+ </div>
215
+ </template>
216
+
217
+ <template #permissions-cell="{ row }">
218
+ <span v-if="isFullAccess(row.original)" class="text-sm">
219
+ {{ t('supabase.access.roles.fullAccess') }}
220
+ </span>
221
+ <span v-else class="text-sm">
222
+ {{
223
+ t(
224
+ 'supabase.access.roles.permissionCount',
225
+ row.original.capabilities.length
226
+ )
227
+ }}
228
+ </span>
229
+ </template>
230
+
231
+ <template #users-cell="{ row }">
232
+ <span class="text-sm">{{
233
+ t('supabase.access.roles.userCount', row.original.user_count)
234
+ }}</span>
235
+ </template>
236
+
237
+ <template #actions-cell="{ row }">
238
+ <div class="flex justify-end">
239
+ <UDropdownMenu
240
+ :items="menuItems(row.original)"
241
+ :content="{ align: 'end' }"
242
+ >
243
+ <UButton
244
+ :aria-label="row.original.label"
245
+ icon="lucide:ellipsis"
246
+ color="neutral"
247
+ variant="ghost"
248
+ />
249
+ </UDropdownMenu>
250
+ </div>
251
+ </template>
252
+ </UTable>
253
+
254
+ <!-- Lazy: the chunk loads on the first open. -->
255
+ <RoleSlideover
256
+ v-if="slideoverMounted"
257
+ v-model:open="slideoverOpen"
258
+ :role="editing"
259
+ @saved="load"
260
+ />
261
+ </AdminView>
262
+ </main>
263
+ </template>