@jskit-ai/users-web 0.1.4
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/package.descriptor.mjs +507 -0
- package/package.json +31 -0
- package/src/client/components/ConsoleSettingsClientElement.vue +24 -0
- package/src/client/components/MembersAdminClientElement.vue +404 -0
- package/src/client/components/ProfileClientElement.vue +242 -0
- package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +39 -0
- package/src/client/components/UsersShellMenuLinkItem.vue +140 -0
- package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +87 -0
- package/src/client/components/UsersWorkspaceMembersMenuItem.vue +36 -0
- package/src/client/components/UsersWorkspacePermissionMenuItem.vue +90 -0
- package/src/client/components/UsersWorkspaceSelector.vue +237 -0
- package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +39 -0
- package/src/client/components/UsersWorkspaceToolsWidget.vue +23 -0
- package/src/client/components/WorkspaceMembersClientElement.vue +663 -0
- package/src/client/components/WorkspaceSettingsClientElement.vue +230 -0
- package/src/client/components/WorkspacesClientElement.vue +514 -0
- package/src/client/composables/accountSettingsAvatarUploadRuntime.js +241 -0
- package/src/client/composables/accountSettingsInvitesRuntime.js +88 -0
- package/src/client/composables/accountSettingsRuntimeConstants.js +77 -0
- package/src/client/composables/accountSettingsRuntimeHelpers.js +75 -0
- package/src/client/composables/errorMessageHelpers.js +66 -0
- package/src/client/composables/internal/useOperationScope.js +144 -0
- package/src/client/composables/modelStateHelpers.js +49 -0
- package/src/client/composables/operationUiHelpers.js +121 -0
- package/src/client/composables/operationValidationHelpers.js +52 -0
- package/src/client/composables/refValueHelpers.js +19 -0
- package/src/client/composables/scopeHelpers.js +145 -0
- package/src/client/composables/useAccess.js +109 -0
- package/src/client/composables/useAccountSettingsRuntime.js +533 -0
- package/src/client/composables/useAddEdit.js +135 -0
- package/src/client/composables/useAddEditCore.js +137 -0
- package/src/client/composables/useBootstrapQuery.js +52 -0
- package/src/client/composables/useCommand.js +112 -0
- package/src/client/composables/useCommandCore.js +130 -0
- package/src/client/composables/useEndpointResource.js +104 -0
- package/src/client/composables/useFieldErrorBag.js +61 -0
- package/src/client/composables/useList.js +85 -0
- package/src/client/composables/useListCore.js +65 -0
- package/src/client/composables/usePagedCollection.js +125 -0
- package/src/client/composables/usePaths.js +108 -0
- package/src/client/composables/useRealtimeQueryInvalidation.js +105 -0
- package/src/client/composables/useScopeRuntime.js +107 -0
- package/src/client/composables/useSurfaceRouteContext.js +31 -0
- package/src/client/composables/useUiFeedback.js +96 -0
- package/src/client/composables/useView.js +89 -0
- package/src/client/composables/useViewCore.js +104 -0
- package/src/client/composables/useWorkspaceRouteContext.js +28 -0
- package/src/client/composables/useWorkspaceSurfaceId.js +43 -0
- package/src/client/index.js +7 -0
- package/src/client/lib/bootstrap.js +95 -0
- package/src/client/lib/httpClient.js +67 -0
- package/src/client/lib/menuIcons.js +192 -0
- package/src/client/lib/permissions.js +34 -0
- package/src/client/lib/profileSurfaceMenuLinks.js +142 -0
- package/src/client/lib/surfaceAccessPolicy.js +350 -0
- package/src/client/lib/theme.js +99 -0
- package/src/client/lib/workspaceLinkResolver.js +207 -0
- package/src/client/lib/workspaceSurfaceContext.js +82 -0
- package/src/client/lib/workspaceSurfacePaths.js +163 -0
- package/src/client/providers/UsersWebClientProvider.js +85 -0
- package/src/client/runtime/bootstrapPlacementRouteGuards.js +371 -0
- package/src/client/runtime/bootstrapPlacementRuntime.js +413 -0
- package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +32 -0
- package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +157 -0
- package/src/client/support/contractGuards.js +34 -0
- package/src/client/support/realtimeWorkspace.js +12 -0
- package/src/client/support/runtimeNormalization.js +27 -0
- package/src/client/support/workspaceQueryKeys.js +15 -0
- package/templates/packages/main/src/client/components/AccountPendingInvitesCue.vue +162 -0
- package/templates/src/components/WorkspaceNotFoundCard.vue +33 -0
- package/templates/src/components/account/settings/AccountSettingsClientElement.vue +153 -0
- package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +77 -0
- package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +55 -0
- package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +125 -0
- package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +94 -0
- package/templates/src/composables/useWorkspaceNotFoundState.js +48 -0
- package/templates/src/pages/account/index.vue +17 -0
- package/templates/src/pages/admin/members/index.vue +7 -0
- package/templates/src/pages/admin/workspace/settings/index.vue +16 -0
- package/templates/src/pages/console/settings/index.vue +16 -0
- package/templates/src/surfaces/admin/index.vue +29 -0
- package/templates/src/surfaces/admin/root.vue +20 -0
- package/templates/src/surfaces/app/index.vue +27 -0
- package/templates/src/surfaces/app/root.vue +20 -0
- package/test/bootstrap.test.js +38 -0
- package/test/bootstrapPlacementRuntime.test.js +991 -0
- package/test/errorMessageHelpers.test.js +28 -0
- package/test/exportsContract.test.js +39 -0
- package/test/menuIcons.test.js +33 -0
- package/test/permissions.test.js +35 -0
- package/test/profileSurfaceMenuLinks.test.js +207 -0
- package/test/refValueHelpers.test.js +14 -0
- package/test/scopeHelpers.test.js +57 -0
- package/test/surfaceAccessPolicy.test.js +129 -0
- package/test/theme.test.js +95 -0
- package/test/workspaceLinkResolver.test.js +61 -0
- package/test/workspaceSurfacePaths.test.js +39 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="members-admin-client-element">
|
|
3
|
+
<v-row>
|
|
4
|
+
<v-col cols="12" lg="5">
|
|
5
|
+
<v-card rounded="lg" elevation="1" border data-testid="members-admin-invite-card">
|
|
6
|
+
<v-card-item>
|
|
7
|
+
<v-card-title class="text-subtitle-1">Invite people</v-card-title>
|
|
8
|
+
<v-card-subtitle>Send workspace invites with a role.</v-card-subtitle>
|
|
9
|
+
</v-card-item>
|
|
10
|
+
<v-divider />
|
|
11
|
+
<v-card-text>
|
|
12
|
+
<template v-if="showWorkspaceInviteLoadingSkeleton">
|
|
13
|
+
<v-skeleton-loader type="text@2, paragraph, button" class="mb-3" />
|
|
14
|
+
</template>
|
|
15
|
+
<template v-else>
|
|
16
|
+
<v-progress-linear v-if="showWorkspaceInviteRefreshingIndicator" indeterminate class="mb-3" />
|
|
17
|
+
<p
|
|
18
|
+
v-if="workspaceInvitePolicyLoaded && !workspaceInvitesAvailable"
|
|
19
|
+
class="text-body-2 text-medium-emphasis mb-3"
|
|
20
|
+
>
|
|
21
|
+
Invites are disabled by app policy or role manifest.
|
|
22
|
+
</p>
|
|
23
|
+
<p
|
|
24
|
+
v-else-if="workspaceInvitePolicyLoaded && !workspaceInvitesEnabled"
|
|
25
|
+
class="text-body-2 text-medium-emphasis mb-3"
|
|
26
|
+
>
|
|
27
|
+
Invites are currently off for this workspace.
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<p v-if="!canInviteMembers" class="text-body-2 text-medium-emphasis mb-3">
|
|
31
|
+
You do not have permission to send invites.
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<template v-else-if="canShowInviteForm">
|
|
35
|
+
<v-form @submit.prevent="onSubmitInvite" novalidate>
|
|
36
|
+
<v-text-field
|
|
37
|
+
v-model="inviteForm.email"
|
|
38
|
+
label="Email"
|
|
39
|
+
variant="outlined"
|
|
40
|
+
density="comfortable"
|
|
41
|
+
type="email"
|
|
42
|
+
autocomplete="email"
|
|
43
|
+
:disabled="isCreatingInvite || showWorkspaceInviteRefreshingIndicator"
|
|
44
|
+
class="mb-3"
|
|
45
|
+
/>
|
|
46
|
+
<v-select
|
|
47
|
+
v-model="inviteForm.roleId"
|
|
48
|
+
label="Role"
|
|
49
|
+
:items="inviteRoleOptions"
|
|
50
|
+
item-title="title"
|
|
51
|
+
item-value="value"
|
|
52
|
+
variant="outlined"
|
|
53
|
+
density="comfortable"
|
|
54
|
+
:disabled="isCreatingInvite || showWorkspaceInviteRefreshingIndicator"
|
|
55
|
+
class="mb-3"
|
|
56
|
+
/>
|
|
57
|
+
<v-btn
|
|
58
|
+
type="submit"
|
|
59
|
+
color="primary"
|
|
60
|
+
:loading="isCreatingInvite"
|
|
61
|
+
:disabled="showWorkspaceInviteRefreshingIndicator"
|
|
62
|
+
>
|
|
63
|
+
Send invite
|
|
64
|
+
</v-btn>
|
|
65
|
+
</v-form>
|
|
66
|
+
</template>
|
|
67
|
+
</template>
|
|
68
|
+
</v-card-text>
|
|
69
|
+
</v-card>
|
|
70
|
+
</v-col>
|
|
71
|
+
|
|
72
|
+
<v-col cols="12" lg="7">
|
|
73
|
+
<v-card rounded="lg" elevation="1" border data-testid="members-admin-members-card">
|
|
74
|
+
<v-card-item>
|
|
75
|
+
<v-card-title class="text-subtitle-1">Team</v-card-title>
|
|
76
|
+
<v-card-subtitle>Members and pending invites.</v-card-subtitle>
|
|
77
|
+
</v-card-item>
|
|
78
|
+
<v-divider />
|
|
79
|
+
<v-card-text>
|
|
80
|
+
<template v-if="showMembersLoadingSkeleton">
|
|
81
|
+
<v-skeleton-loader type="text@2, list-item-avatar-two-line@3" class="mb-3" />
|
|
82
|
+
<v-divider class="mb-3" />
|
|
83
|
+
<v-skeleton-loader type="text, list-item-two-line@2" />
|
|
84
|
+
</template>
|
|
85
|
+
<template v-else>
|
|
86
|
+
<v-progress-linear v-if="showMembersRefreshingIndicator" indeterminate class="mb-3" />
|
|
87
|
+
<p v-if="!canViewMembers" class="text-body-2 text-medium-emphasis mb-0">
|
|
88
|
+
You do not have permission to view members.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<template v-else>
|
|
92
|
+
<div class="text-caption text-medium-emphasis mb-2">Members</div>
|
|
93
|
+
<v-list density="comfortable" class="pa-0 mb-3">
|
|
94
|
+
<v-list-item v-for="member in memberRows" :key="member.userId" class="px-0">
|
|
95
|
+
<template #title>
|
|
96
|
+
<div class="d-flex align-center ga-2">
|
|
97
|
+
<span>{{ member.displayName || member.email }}</span>
|
|
98
|
+
<v-chip v-if="showOwnerChip(member)" size="x-small" label color="secondary">Owner</v-chip>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
<template #subtitle>
|
|
102
|
+
{{ member.email }}
|
|
103
|
+
</template>
|
|
104
|
+
|
|
105
|
+
<template #append>
|
|
106
|
+
<div class="d-flex align-center ga-2">
|
|
107
|
+
<v-select
|
|
108
|
+
v-model="member.roleId"
|
|
109
|
+
:items="memberRoleOptions"
|
|
110
|
+
item-title="title"
|
|
111
|
+
item-value="value"
|
|
112
|
+
density="compact"
|
|
113
|
+
variant="outlined"
|
|
114
|
+
hide-details
|
|
115
|
+
class="member-role-select"
|
|
116
|
+
:disabled="showMembersRefreshingIndicator || isMemberRoleLocked(member)"
|
|
117
|
+
@update:model-value="(value) => onMemberRoleUpdate(member, value)"
|
|
118
|
+
/>
|
|
119
|
+
<v-btn
|
|
120
|
+
variant="text"
|
|
121
|
+
color="error"
|
|
122
|
+
:disabled="showMembersRefreshingIndicator || isMemberRemoveLocked(member)"
|
|
123
|
+
:loading="isRemoveMemberLoading(member.userId)"
|
|
124
|
+
@click="onRemoveMember(member)"
|
|
125
|
+
>
|
|
126
|
+
Remove
|
|
127
|
+
</v-btn>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
130
|
+
</v-list-item>
|
|
131
|
+
</v-list>
|
|
132
|
+
|
|
133
|
+
<v-divider class="mb-3" />
|
|
134
|
+
|
|
135
|
+
<div class="text-caption text-medium-emphasis mb-2">Pending invites</div>
|
|
136
|
+
<v-list density="comfortable" class="pa-0">
|
|
137
|
+
<v-list-item v-for="invite in inviteRows" :key="invite.id" class="px-0">
|
|
138
|
+
<template #title>
|
|
139
|
+
{{ invite.email }}
|
|
140
|
+
</template>
|
|
141
|
+
<template #subtitle>
|
|
142
|
+
Role: {{ invite.roleId }} • expires {{ formatDateTime(invite.expiresAt) }}
|
|
143
|
+
</template>
|
|
144
|
+
<template #append>
|
|
145
|
+
<v-btn
|
|
146
|
+
v-if="canRevokeInvites"
|
|
147
|
+
variant="text"
|
|
148
|
+
color="error"
|
|
149
|
+
:disabled="showMembersRefreshingIndicator"
|
|
150
|
+
:loading="isRevokeInviteLoading(invite.id)"
|
|
151
|
+
@click="onRevokeInvite(invite.id)"
|
|
152
|
+
>
|
|
153
|
+
Revoke
|
|
154
|
+
</v-btn>
|
|
155
|
+
</template>
|
|
156
|
+
</v-list-item>
|
|
157
|
+
<p v-if="inviteRows.length < 1" class="text-body-2 text-medium-emphasis mb-0">No pending invites.</p>
|
|
158
|
+
</v-list>
|
|
159
|
+
</template>
|
|
160
|
+
</template>
|
|
161
|
+
</v-card-text>
|
|
162
|
+
</v-card>
|
|
163
|
+
</v-col>
|
|
164
|
+
</v-row>
|
|
165
|
+
</section>
|
|
166
|
+
</template>
|
|
167
|
+
|
|
168
|
+
<script setup>
|
|
169
|
+
import { computed, toRefs, unref } from "vue";
|
|
170
|
+
import { requireBoolean, requireFunction, requireRecord } from "../support/contractGuards.js";
|
|
171
|
+
|
|
172
|
+
const props = defineProps({
|
|
173
|
+
forms: {
|
|
174
|
+
type: Object,
|
|
175
|
+
required: true
|
|
176
|
+
},
|
|
177
|
+
options: {
|
|
178
|
+
type: Object,
|
|
179
|
+
required: true
|
|
180
|
+
},
|
|
181
|
+
collections: {
|
|
182
|
+
type: Object,
|
|
183
|
+
required: true
|
|
184
|
+
},
|
|
185
|
+
permissions: {
|
|
186
|
+
type: Object,
|
|
187
|
+
required: true
|
|
188
|
+
},
|
|
189
|
+
revokeInviteId: {
|
|
190
|
+
type: Number,
|
|
191
|
+
required: true
|
|
192
|
+
},
|
|
193
|
+
removeMemberUserId: {
|
|
194
|
+
type: Number,
|
|
195
|
+
required: true
|
|
196
|
+
},
|
|
197
|
+
status: {
|
|
198
|
+
type: Object,
|
|
199
|
+
required: true
|
|
200
|
+
},
|
|
201
|
+
actions: {
|
|
202
|
+
type: Object,
|
|
203
|
+
required: true
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
requireRecord(props.forms, "forms", "MembersAdminClientElement");
|
|
208
|
+
requireRecord(props.options, "options", "MembersAdminClientElement");
|
|
209
|
+
requireRecord(props.collections, "collections", "MembersAdminClientElement");
|
|
210
|
+
requireRecord(props.permissions, "permissions", "MembersAdminClientElement");
|
|
211
|
+
requireRecord(props.status, "status", "MembersAdminClientElement");
|
|
212
|
+
requireRecord(props.actions, "actions", "MembersAdminClientElement");
|
|
213
|
+
|
|
214
|
+
const {
|
|
215
|
+
forms,
|
|
216
|
+
options,
|
|
217
|
+
collections,
|
|
218
|
+
permissions,
|
|
219
|
+
revokeInviteId,
|
|
220
|
+
removeMemberUserId,
|
|
221
|
+
status,
|
|
222
|
+
actions
|
|
223
|
+
} = toRefs(props);
|
|
224
|
+
|
|
225
|
+
const actionHandlers = Object.freeze({
|
|
226
|
+
submitInvite: requireFunction(actions.value.submitInvite, "actions.submitInvite", "MembersAdminClientElement"),
|
|
227
|
+
submitRevokeInvite: requireFunction(
|
|
228
|
+
actions.value.submitRevokeInvite,
|
|
229
|
+
"actions.submitRevokeInvite",
|
|
230
|
+
"MembersAdminClientElement"
|
|
231
|
+
),
|
|
232
|
+
submitMemberRoleUpdate: requireFunction(
|
|
233
|
+
actions.value.submitMemberRoleUpdate,
|
|
234
|
+
"actions.submitMemberRoleUpdate",
|
|
235
|
+
"MembersAdminClientElement"
|
|
236
|
+
),
|
|
237
|
+
submitRemoveMember: requireFunction(
|
|
238
|
+
actions.value.submitRemoveMember,
|
|
239
|
+
"actions.submitRemoveMember",
|
|
240
|
+
"MembersAdminClientElement"
|
|
241
|
+
)
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const inviteForm = computed(() => requireRecord(forms.value.invite, "forms.invite", "MembersAdminClientElement"));
|
|
245
|
+
const workspaceForm = computed(() =>
|
|
246
|
+
requireRecord(forms.value.workspace, "forms.workspace", "MembersAdminClientElement")
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const memberRows = computed(() => {
|
|
250
|
+
const source = collections.value.members;
|
|
251
|
+
return Array.isArray(unref(source)) ? unref(source) : [];
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const inviteRows = computed(() => {
|
|
255
|
+
const source = collections.value.invites;
|
|
256
|
+
return Array.isArray(unref(source)) ? unref(source) : [];
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const inviteRoleOptions = computed(() => {
|
|
260
|
+
const source = options.value.inviteRoleOptions;
|
|
261
|
+
return Array.isArray(unref(source)) ? unref(source) : [];
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const memberRoleOptions = computed(() => {
|
|
265
|
+
const source = options.value.memberRoleOptions;
|
|
266
|
+
return Array.isArray(unref(source)) ? unref(source) : [];
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const canViewMembers = computed(() => Boolean(unref(permissions.value.canViewMembers)));
|
|
270
|
+
const canInviteMembers = computed(() => Boolean(unref(permissions.value.canInviteMembers)));
|
|
271
|
+
const canManageMembers = computed(() => Boolean(unref(permissions.value.canManageMembers)));
|
|
272
|
+
const canRevokeInvites = computed(() => Boolean(unref(permissions.value.canRevokeInvites)));
|
|
273
|
+
const isCreatingInvite = computed(() => Boolean(unref(status.value.isCreatingInvite)));
|
|
274
|
+
const isRevokingInvite = computed(() => Boolean(unref(status.value.isRevokingInvite)));
|
|
275
|
+
const isRemovingMember = computed(() => Boolean(unref(status.value.isRemovingMember)));
|
|
276
|
+
const workspaceInvitePolicyLoaded = computed(() =>
|
|
277
|
+
requireBoolean(status.value.hasLoadedWorkspaceSettings, "status.hasLoadedWorkspaceSettings", "MembersAdminClientElement")
|
|
278
|
+
);
|
|
279
|
+
const workspaceInvitePolicyRefreshing = computed(() =>
|
|
280
|
+
requireBoolean(
|
|
281
|
+
status.value.isRefreshingWorkspaceSettings,
|
|
282
|
+
"status.isRefreshingWorkspaceSettings",
|
|
283
|
+
"MembersAdminClientElement"
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
const membersListLoaded = computed(() =>
|
|
287
|
+
requireBoolean(status.value.hasLoadedMembersList, "status.hasLoadedMembersList", "MembersAdminClientElement")
|
|
288
|
+
);
|
|
289
|
+
const membersListRefreshing = computed(() =>
|
|
290
|
+
requireBoolean(status.value.isRefreshingMembersList, "status.isRefreshingMembersList", "MembersAdminClientElement")
|
|
291
|
+
);
|
|
292
|
+
const inviteListLoaded = computed(() =>
|
|
293
|
+
requireBoolean(status.value.hasLoadedInviteList, "status.hasLoadedInviteList", "MembersAdminClientElement")
|
|
294
|
+
);
|
|
295
|
+
const inviteListRefreshing = computed(() =>
|
|
296
|
+
requireBoolean(status.value.isRefreshingInviteList, "status.isRefreshingInviteList", "MembersAdminClientElement")
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const showWorkspaceInviteLoadingSkeleton = computed(
|
|
300
|
+
() => canInviteMembers.value && !workspaceInvitePolicyLoaded.value
|
|
301
|
+
);
|
|
302
|
+
const showWorkspaceInviteRefreshingIndicator = computed(
|
|
303
|
+
() => canInviteMembers.value && workspaceInvitePolicyLoaded.value && workspaceInvitePolicyRefreshing.value
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const showMembersLoadingSkeleton = computed(
|
|
307
|
+
() =>
|
|
308
|
+
canViewMembers.value &&
|
|
309
|
+
(!membersListLoaded.value || !inviteListLoaded.value)
|
|
310
|
+
);
|
|
311
|
+
const showMembersRefreshingIndicator = computed(
|
|
312
|
+
() =>
|
|
313
|
+
canViewMembers.value &&
|
|
314
|
+
membersListLoaded.value &&
|
|
315
|
+
inviteListLoaded.value &&
|
|
316
|
+
(membersListRefreshing.value || inviteListRefreshing.value)
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const workspaceInvitesAvailable = computed(() => Boolean(unref(workspaceForm.value.invitesAvailable)));
|
|
320
|
+
const workspaceInvitesEnabled = computed(() => Boolean(unref(workspaceForm.value.invitesEnabled)));
|
|
321
|
+
|
|
322
|
+
const canShowInviteForm = computed(
|
|
323
|
+
() => canInviteMembers.value && workspaceInvitesAvailable.value && workspaceInvitesEnabled.value
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
function formatDateTime(value) {
|
|
327
|
+
if (typeof options.value.formatDateTime === "function") {
|
|
328
|
+
return options.value.formatDateTime(value);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const date = new Date(value);
|
|
332
|
+
if (Number.isNaN(date.getTime())) {
|
|
333
|
+
return "unknown";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return date.toLocaleString();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function showOwnerChip(member) {
|
|
340
|
+
return Boolean(member?.isOwner);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isMemberRoleLocked(member) {
|
|
344
|
+
if (!canManageMembers.value) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return Boolean(member?.isOwner);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function isMemberRemoveLocked(member) {
|
|
352
|
+
if (!canManageMembers.value) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return Boolean(member?.isOwner);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isRevokeInviteLoading(inviteId) {
|
|
360
|
+
return isRevokingInvite.value && revokeInviteId.value === Number(inviteId || 0);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function isRemoveMemberLoading(memberUserId) {
|
|
364
|
+
return isRemovingMember.value && removeMemberUserId.value === Number(memberUserId || 0);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function onSubmitInvite() {
|
|
368
|
+
if (!canShowInviteForm.value) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
await actionHandlers.submitInvite();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function onRevokeInvite(inviteId) {
|
|
376
|
+
if (!canRevokeInvites.value) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
await actionHandlers.submitRevokeInvite(inviteId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function onMemberRoleUpdate(member, roleId) {
|
|
384
|
+
if (isMemberRoleLocked(member)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
await actionHandlers.submitMemberRoleUpdate(member, roleId);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function onRemoveMember(member) {
|
|
392
|
+
if (isMemberRemoveLocked(member)) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await actionHandlers.submitRemoveMember(member);
|
|
397
|
+
}
|
|
398
|
+
</script>
|
|
399
|
+
|
|
400
|
+
<style scoped>
|
|
401
|
+
.member-role-select {
|
|
402
|
+
width: 160px;
|
|
403
|
+
}
|
|
404
|
+
</style>
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section :class="rootClasses" :data-testid="uiTestIds.root">
|
|
3
|
+
<v-card
|
|
4
|
+
class="profile-client-card"
|
|
5
|
+
:class="uiClasses.card"
|
|
6
|
+
:rounded="resolvedVariant.surface === 'plain' ? '0' : 'lg'"
|
|
7
|
+
:elevation="0"
|
|
8
|
+
:border="resolvedVariant.surface !== 'plain'"
|
|
9
|
+
:variant="resolvedVariant.surface === 'plain' ? 'text' : undefined"
|
|
10
|
+
:data-testid="uiTestIds.card"
|
|
11
|
+
>
|
|
12
|
+
<v-card-item v-if="resolvedFeatures.header">
|
|
13
|
+
<v-card-title class="text-subtitle-1">{{ copyText.title }}</v-card-title>
|
|
14
|
+
</v-card-item>
|
|
15
|
+
<v-divider v-if="resolvedFeatures.header" />
|
|
16
|
+
<v-card-text>
|
|
17
|
+
<slot name="form-before" :state="state" :actions="actions" />
|
|
18
|
+
|
|
19
|
+
<v-form @submit.prevent="onSubmitProfile" novalidate>
|
|
20
|
+
<v-row class="mb-2">
|
|
21
|
+
<v-col cols="12" md="4" class="d-flex flex-column align-center justify-center">
|
|
22
|
+
<v-avatar :size="state.preferencesForm.avatarSize" color="surface-variant" rounded="circle" class="mb-3">
|
|
23
|
+
<v-img v-if="state.profileAvatar.effectiveUrl" :src="state.profileAvatar.effectiveUrl" cover />
|
|
24
|
+
<span v-else class="text-h6">{{ state.profileInitials }}</span>
|
|
25
|
+
</v-avatar>
|
|
26
|
+
<div class="text-caption text-medium-emphasis">
|
|
27
|
+
{{ copyText.previewSizePrefix }} {{ state.preferencesForm.avatarSize }} {{ copyText.previewSizeSuffix }}
|
|
28
|
+
</div>
|
|
29
|
+
</v-col>
|
|
30
|
+
<v-col cols="12" md="8">
|
|
31
|
+
<div class="d-flex flex-wrap ga-2 mb-2">
|
|
32
|
+
<v-btn variant="tonal" color="secondary" :data-testid="uiTestIds.avatarReplaceButton" @click="onAvatarReplace">
|
|
33
|
+
{{ copyText.replaceAvatar }}
|
|
34
|
+
</v-btn>
|
|
35
|
+
<v-btn
|
|
36
|
+
v-if="resolvedFeatures.removeAvatar && state.profileAvatar.hasUploadedAvatar"
|
|
37
|
+
variant="text"
|
|
38
|
+
color="error"
|
|
39
|
+
:data-testid="uiTestIds.avatarRemoveButton"
|
|
40
|
+
:loading="state.avatarDeleteMutation.isPending.value"
|
|
41
|
+
@click="onAvatarRemove"
|
|
42
|
+
>
|
|
43
|
+
{{ copyText.removeAvatar }}
|
|
44
|
+
</v-btn>
|
|
45
|
+
<slot name="avatar-actions-extra" :state="state" :actions="actions" />
|
|
46
|
+
</div>
|
|
47
|
+
<div v-if="state.selectedAvatarFileName" class="text-caption text-medium-emphasis mb-2">
|
|
48
|
+
{{ copyText.selectedFilePrefix }} {{ state.selectedAvatarFileName }}
|
|
49
|
+
</div>
|
|
50
|
+
</v-col>
|
|
51
|
+
</v-row>
|
|
52
|
+
|
|
53
|
+
<v-row>
|
|
54
|
+
<v-col cols="12" md="6">
|
|
55
|
+
<v-text-field
|
|
56
|
+
v-model="state.profileForm.displayName"
|
|
57
|
+
:label="copyText.displayName"
|
|
58
|
+
variant="outlined"
|
|
59
|
+
:density="resolvedVariant.density"
|
|
60
|
+
autocomplete="nickname"
|
|
61
|
+
:error-messages="state.profileFieldErrors.displayName ? [state.profileFieldErrors.displayName] : []"
|
|
62
|
+
/>
|
|
63
|
+
</v-col>
|
|
64
|
+
<v-col cols="12" md="6">
|
|
65
|
+
<v-text-field
|
|
66
|
+
v-model="state.profileForm.email"
|
|
67
|
+
:label="copyText.email"
|
|
68
|
+
variant="outlined"
|
|
69
|
+
:density="resolvedVariant.density"
|
|
70
|
+
readonly
|
|
71
|
+
:hint="copyText.emailHint"
|
|
72
|
+
persistent-hint
|
|
73
|
+
/>
|
|
74
|
+
</v-col>
|
|
75
|
+
</v-row>
|
|
76
|
+
<v-btn type="submit" color="primary" :loading="state.profileMutation.isPending.value" :data-testid="uiTestIds.submitButton">
|
|
77
|
+
{{ copyText.saveProfile }}
|
|
78
|
+
</v-btn>
|
|
79
|
+
|
|
80
|
+
<slot name="form-after" :state="state" :actions="actions" />
|
|
81
|
+
</v-form>
|
|
82
|
+
|
|
83
|
+
<slot name="footer-extra" :state="state" :actions="actions" />
|
|
84
|
+
</v-card-text>
|
|
85
|
+
</v-card>
|
|
86
|
+
</section>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<script setup>
|
|
90
|
+
import { computed } from "vue";
|
|
91
|
+
import { createComponentInteractionEmitter } from "@jskit-ai/kernel/client";
|
|
92
|
+
import {
|
|
93
|
+
normalizeObject,
|
|
94
|
+
normalizeOneOf
|
|
95
|
+
} from "@jskit-ai/kernel/shared/support/normalize";
|
|
96
|
+
|
|
97
|
+
const DEFAULT_COPY = Object.freeze({
|
|
98
|
+
title: "Profile",
|
|
99
|
+
previewSizePrefix: "Preview size:",
|
|
100
|
+
previewSizeSuffix: "px",
|
|
101
|
+
replaceAvatar: "Replace avatar",
|
|
102
|
+
removeAvatar: "Remove avatar",
|
|
103
|
+
selectedFilePrefix: "Selected file:",
|
|
104
|
+
displayName: "Display name",
|
|
105
|
+
email: "Email",
|
|
106
|
+
emailHint: "Managed by Supabase Auth",
|
|
107
|
+
saveProfile: "Save profile"
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const props = defineProps({
|
|
111
|
+
state: {
|
|
112
|
+
type: Object,
|
|
113
|
+
required: true
|
|
114
|
+
},
|
|
115
|
+
actions: {
|
|
116
|
+
type: Object,
|
|
117
|
+
required: true
|
|
118
|
+
},
|
|
119
|
+
copy: {
|
|
120
|
+
type: Object,
|
|
121
|
+
default: () => ({})
|
|
122
|
+
},
|
|
123
|
+
variant: {
|
|
124
|
+
type: Object,
|
|
125
|
+
default: () => ({})
|
|
126
|
+
},
|
|
127
|
+
features: {
|
|
128
|
+
type: Object,
|
|
129
|
+
default: () => ({})
|
|
130
|
+
},
|
|
131
|
+
ui: {
|
|
132
|
+
type: Object,
|
|
133
|
+
default: () => ({})
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const emit = defineEmits(["action:started", "action:succeeded", "action:failed", "interaction", "profile:submit", "avatar:replace", "avatar:remove"]);
|
|
138
|
+
|
|
139
|
+
const state = props.state;
|
|
140
|
+
const actions = props.actions;
|
|
141
|
+
const {
|
|
142
|
+
emitInteraction,
|
|
143
|
+
invokeAction
|
|
144
|
+
} = createComponentInteractionEmitter(emit);
|
|
145
|
+
|
|
146
|
+
const copyText = computed(() => ({
|
|
147
|
+
...DEFAULT_COPY,
|
|
148
|
+
...normalizeObject(props.copy)
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
const resolvedVariant = computed(() => {
|
|
152
|
+
const variant = normalizeObject(props.variant);
|
|
153
|
+
return {
|
|
154
|
+
layout: normalizeOneOf(variant.layout, ["compact", "comfortable"], "comfortable"),
|
|
155
|
+
surface: normalizeOneOf(variant.surface, ["plain", "carded"], "carded"),
|
|
156
|
+
density: normalizeOneOf(variant.density, ["compact", "comfortable"], "comfortable"),
|
|
157
|
+
tone: normalizeOneOf(variant.tone, ["neutral", "emphasized"], "neutral")
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const resolvedFeatures = computed(() => {
|
|
162
|
+
const features = normalizeObject(props.features);
|
|
163
|
+
return {
|
|
164
|
+
header: features.header !== false,
|
|
165
|
+
removeAvatar: features.removeAvatar !== false
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const uiClasses = computed(() => {
|
|
170
|
+
const classes = normalizeObject(normalizeObject(props.ui).classes);
|
|
171
|
+
return {
|
|
172
|
+
root: String(classes.root || "").trim(),
|
|
173
|
+
card: String(classes.card || "").trim()
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const uiTestIds = computed(() => {
|
|
178
|
+
const testIds = normalizeObject(normalizeObject(props.ui).testIds);
|
|
179
|
+
return {
|
|
180
|
+
root: String(testIds.root || "profile-client-element"),
|
|
181
|
+
card: String(testIds.card || "profile-client-card"),
|
|
182
|
+
submitButton: String(testIds.submitButton || "profile-submit-button"),
|
|
183
|
+
avatarReplaceButton: String(testIds.avatarReplaceButton || "profile-avatar-replace-button"),
|
|
184
|
+
avatarRemoveButton: String(testIds.avatarRemoveButton || "profile-avatar-remove-button")
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const rootClasses = computed(() => {
|
|
189
|
+
const classes = [
|
|
190
|
+
"profile-client-element",
|
|
191
|
+
`profile-client-element--layout-${resolvedVariant.value.layout}`,
|
|
192
|
+
`profile-client-element--surface-${resolvedVariant.value.surface}`,
|
|
193
|
+
`profile-client-element--density-${resolvedVariant.value.density}`,
|
|
194
|
+
`profile-client-element--tone-${resolvedVariant.value.tone}`
|
|
195
|
+
];
|
|
196
|
+
if (uiClasses.value.root) {
|
|
197
|
+
classes.push(uiClasses.value.root);
|
|
198
|
+
}
|
|
199
|
+
return classes;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
async function onSubmitProfile() {
|
|
203
|
+
const payload = {
|
|
204
|
+
displayName: String(state.profileForm?.displayName || "").trim()
|
|
205
|
+
};
|
|
206
|
+
emit("profile:submit", payload);
|
|
207
|
+
emitInteraction("profile:submit", payload);
|
|
208
|
+
await invokeAction("submitProfile", payload, actions.submitProfile);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function onAvatarReplace() {
|
|
212
|
+
emit("avatar:replace", {});
|
|
213
|
+
emitInteraction("avatar:replace");
|
|
214
|
+
await invokeAction("openAvatarEditor", {}, actions.openAvatarEditor);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function onAvatarRemove() {
|
|
218
|
+
emit("avatar:remove", {});
|
|
219
|
+
emitInteraction("avatar:remove");
|
|
220
|
+
await invokeAction("submitAvatarDelete", {}, actions.submitAvatarDelete);
|
|
221
|
+
}
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<style scoped>
|
|
225
|
+
.profile-client-element--layout-compact :deep(.v-card-item),
|
|
226
|
+
.profile-client-element--layout-compact :deep(.v-card-text) {
|
|
227
|
+
padding-block: 0.72rem;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.profile-client-element--surface-plain .profile-client-card {
|
|
231
|
+
box-shadow: none;
|
|
232
|
+
border-width: 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.profile-client-element--density-compact :deep(.v-field__input) {
|
|
236
|
+
min-height: 34px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.profile-client-element--tone-emphasized :deep(.v-avatar) {
|
|
240
|
+
box-shadow: 0 0 0 2px rgba(var(--v-theme-primary), 0.18);
|
|
241
|
+
}
|
|
242
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useSurfaceRouteContext } from "../composables/useSurfaceRouteContext.js";
|
|
4
|
+
import { resolveProfileSurfaceMenuLinks } from "../lib/profileSurfaceMenuLinks.js";
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
surface: {
|
|
8
|
+
type: String,
|
|
9
|
+
default: "*"
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const { placementContext, currentSurfaceId } = useSurfaceRouteContext();
|
|
14
|
+
|
|
15
|
+
const resolvedSurfaceId = computed(() => {
|
|
16
|
+
const explicitSurface = String(props.surface || "").trim().toLowerCase();
|
|
17
|
+
if (explicitSurface && explicitSurface !== "*") {
|
|
18
|
+
return explicitSurface;
|
|
19
|
+
}
|
|
20
|
+
return String(currentSurfaceId.value || "").trim().toLowerCase() || "*";
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const resolvedLinks = computed(() => {
|
|
24
|
+
return resolveProfileSurfaceMenuLinks({
|
|
25
|
+
context: placementContext.value,
|
|
26
|
+
surface: resolvedSurfaceId.value
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<v-list-item
|
|
33
|
+
v-for="link in resolvedLinks"
|
|
34
|
+
:key="link.id"
|
|
35
|
+
:title="link.label"
|
|
36
|
+
:to="link.to"
|
|
37
|
+
:prepend-icon="link.icon"
|
|
38
|
+
/>
|
|
39
|
+
</template>
|