@7365admin1/layer-common 4.91.0 → 4.93.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 +24 -4
- package/components/InvitationForm.vue +31 -4
- package/components/InvitationMain.vue +124 -22
- package/composables/useUser.ts +6 -2
- package/composables/useVerification.ts +3 -0
- package/middleware/01.auth.ts +3 -7
- package/package.json +1 -1
- package/utils/invite-group.ts +61 -0
- package/utils/invite-sites.ts +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.93.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b763487: Invitations list shows ONE row per multi-site invitation (e-mail, role, date, "N sites" with a details dialog listing every site and its status). Row actions: Resend (one request, one e-mail for the whole group), Cancel all, and Cancel one site inside the details. Old single-site invitations look and behave as before. Asks the API for `group=true`; against an API that does not know it, falls back to the plain per-site list. Same component on the org console and every app's Invitations page.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 31c71b3: The auth middleware no longer saves `?email=` as a browser-wide `service-provider-email` cookie (30 days). Nothing reads it any more, and a leftover from another account sent the org app's sign-in to that account's create page. Redirect-to-sign-in behaviour is unchanged.
|
|
12
|
+
|
|
13
|
+
## 4.92.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 9883bfd: Invite member: pick several sites in one invitation.
|
|
18
|
+
|
|
19
|
+
- `InvitationForm`: when it is the org-level "Invite member" (no fixed site), the Site field is a multi-select with removable chips (at least one site required). One site: roles are that site's roles plus organisation roles, as before. Two or more: only organisation roles are offered (a site role is valid at its own site only), with a hint saying so and how to give different roles per site. Site apps and onboarding keep their fixed single site.
|
|
20
|
+
- `useUser().inviteMemberClient` takes `sites`: one site sends `siteId` as before, several send `siteIds` (needs the matching `@7365admin1/core` release; older core ignores it). New helper `utils/invite-sites.ts`.
|
|
21
|
+
- `InvitationMain`: the Organization app's Invitations list no longer filters to `app: organization`, so site invitations sent from there are listed (the "Site Invited To" column shows each site). Site apps are unchanged.
|
|
22
|
+
|
|
3
23
|
## 4.91.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
|
@@ -2034,11 +2054,11 @@
|
|
|
2034
2054
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
2035
2055
|
own rule from two endpoints the console already calls, unprojected:
|
|
2036
2056
|
|
|
2037
|
-
|
|
2038
|
-
|
|
2057
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
2058
|
+
GET /api/roles/id/:role that membership's role document
|
|
2039
2059
|
|
|
2040
|
-
|
|
2041
|
-
|
|
2060
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
2061
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
2042
2062
|
|
|
2043
2063
|
`role.default` is the marker because it is the only property of a platform
|
|
2044
2064
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -88,13 +88,24 @@
|
|
|
88
88
|
persistent-hint
|
|
89
89
|
data-test="invite-fixed-site"
|
|
90
90
|
></v-text-field>
|
|
91
|
+
<!--
|
|
92
|
+
Several sites (owner request 2026-10-02): one invitation per
|
|
93
|
+
site, one e-mail. One role for all of them, so 2+ sites offer
|
|
94
|
+
organisation roles only (see `roleSiteFor`).
|
|
95
|
+
-->
|
|
91
96
|
<v-autocomplete
|
|
92
97
|
v-else
|
|
93
|
-
v-model="invite.
|
|
98
|
+
v-model="invite.sites"
|
|
94
99
|
:items="sites"
|
|
100
|
+
multiple
|
|
101
|
+
chips
|
|
102
|
+
closable-chips
|
|
95
103
|
class="filter-field"
|
|
96
104
|
density="comfortable"
|
|
97
|
-
:rules="[
|
|
105
|
+
:rules="[sitesRule]"
|
|
106
|
+
:hint="invite.sites.length > 1 ? severalSitesHint : ''"
|
|
107
|
+
:persistent-hint="invite.sites.length > 1"
|
|
108
|
+
data-test="invite-sites"
|
|
98
109
|
@update:model-value="handleUpdateSite"
|
|
99
110
|
></v-autocomplete>
|
|
100
111
|
</v-col>
|
|
@@ -163,6 +174,7 @@ import { useLocalSetup } from '../composables/useLocalSetup';
|
|
|
163
174
|
import useRole from '../composables/useRole';
|
|
164
175
|
import useSite from '../composables/useSite';
|
|
165
176
|
import { inviteRoleOptions } from '../utils/invite-role-options';
|
|
177
|
+
import { roleSiteFor } from '../utils/invite-sites';
|
|
166
178
|
import useUser from '../composables/useUser';
|
|
167
179
|
import useUtils from '../composables/useUtils';
|
|
168
180
|
|
|
@@ -276,6 +288,8 @@ const invite = ref<Record<string, any>>({
|
|
|
276
288
|
role: "",
|
|
277
289
|
org: "",
|
|
278
290
|
site: "",
|
|
291
|
+
// the sites picked in the Site field (several allowed); `site` follows it
|
|
292
|
+
sites: [] as string[],
|
|
279
293
|
siteName: "",
|
|
280
294
|
isMemberInvite: false,
|
|
281
295
|
});
|
|
@@ -501,9 +515,17 @@ watchEffect(() => {
|
|
|
501
515
|
|
|
502
516
|
const roleOptions = computed(() => inviteRoleOptions(roles.value));
|
|
503
517
|
|
|
518
|
+
const severalSitesHint =
|
|
519
|
+
"Several sites: pick an organisation role - it applies at each site. For a different role per site, invite one site at a time.";
|
|
520
|
+
|
|
521
|
+
const sitesRule = (v: string[]) =>
|
|
522
|
+
(Array.isArray(v) && v.length > 0) || "Pick at least one site";
|
|
523
|
+
|
|
504
524
|
const noRolesText = computed(() =>
|
|
505
525
|
rolesError?.value
|
|
506
526
|
? "The roles could not be loaded. You may not have access to this organisation's roles; refresh, or sign in again."
|
|
527
|
+
: !fixedSite.value && invite.value.sites.length > 1
|
|
528
|
+
? "No organisation roles for this service yet. Create one in Roles & Permissions (Roles for <service>, use at any site), or invite one site at a time."
|
|
507
529
|
: fixedSite.value
|
|
508
530
|
? "No roles have been created for this site yet. Create one in Roles & Permissions first."
|
|
509
531
|
: "No roles have been created yet. Create one in Roles & Permissions first."
|
|
@@ -525,6 +547,7 @@ const rolesPageLink = computed(() => {
|
|
|
525
547
|
async function handleUpdateApp(value: string) {
|
|
526
548
|
invite.value.role = "";
|
|
527
549
|
if (!fixedSite.value) {
|
|
550
|
+
invite.value.sites = [];
|
|
528
551
|
invite.value.site = "";
|
|
529
552
|
invite.value.siteName = "";
|
|
530
553
|
}
|
|
@@ -544,12 +567,16 @@ function resetInvite() {
|
|
|
544
567
|
invite.value.role = "";
|
|
545
568
|
// Same site as before: a fixed site stays fixed, a picked one is cleared.
|
|
546
569
|
invite.value.site = fixedSite.value;
|
|
570
|
+
invite.value.sites = [];
|
|
547
571
|
if (!invite.value.site) invite.value.siteName = "";
|
|
548
572
|
message.value = "";
|
|
549
573
|
}
|
|
550
574
|
|
|
551
|
-
function handleUpdateSite(
|
|
552
|
-
|
|
575
|
+
function handleUpdateSite(siteIds: string[]) {
|
|
576
|
+
// The roles list follows `site`: the one chosen site, or "" for several
|
|
577
|
+
// (organisation roles only); the role watch clears a role no longer offered.
|
|
578
|
+
invite.value.site = roleSiteFor(siteIds);
|
|
579
|
+
const obj = sites.value.find((x) => x?.value === invite.value.site);
|
|
553
580
|
invite.value.siteName = obj?.title || "";
|
|
554
581
|
}
|
|
555
582
|
|
|
@@ -61,19 +61,37 @@
|
|
|
61
61
|
<template #item.createdAt="{ item }">
|
|
62
62
|
{{ item.createdAt ? formatDate(item.createdAt) : "" }}
|
|
63
63
|
</template>
|
|
64
|
+
<template #item.siteLabel="{ item }">
|
|
65
|
+
<a
|
|
66
|
+
v-if="siteCountOf(item) > 1"
|
|
67
|
+
href="#"
|
|
68
|
+
class="text-primary"
|
|
69
|
+
:title="item.sites?.map((s) => s.siteName).join(', ')"
|
|
70
|
+
@click.prevent="detailItem = item"
|
|
71
|
+
>
|
|
72
|
+
{{ siteLabel(item) }}
|
|
73
|
+
</a>
|
|
74
|
+
<span v-else>{{ siteLabel(item) }}</span>
|
|
75
|
+
</template>
|
|
64
76
|
<template #item.action-table="{ item }">
|
|
65
77
|
<v-menu
|
|
66
|
-
:close-on-content-click="
|
|
78
|
+
:close-on-content-click="true"
|
|
67
79
|
offset-y
|
|
68
|
-
width="
|
|
69
|
-
v-if="props.cancelInvitation"
|
|
80
|
+
width="170"
|
|
81
|
+
v-if="item.status === 'pending' && (props.cancelInvitation || props.inviteMember)"
|
|
70
82
|
>
|
|
71
83
|
<template v-slot:activator="{ props }">
|
|
72
84
|
<v-icon v-bind="props">mdi-dots-vertical</v-icon>
|
|
73
85
|
</template>
|
|
74
86
|
<v-list>
|
|
75
|
-
<v-list-item
|
|
76
|
-
|
|
87
|
+
<v-list-item v-if="props.inviteMember" @click="onResend(item)">
|
|
88
|
+
Resend
|
|
89
|
+
</v-list-item>
|
|
90
|
+
<v-list-item
|
|
91
|
+
v-if="props.cancelInvitation"
|
|
92
|
+
@click="openConfirmDialog(groupIds(item))"
|
|
93
|
+
>
|
|
94
|
+
{{ siteCountOf(item) > 1 ? "Cancel all" : "Cancel Invite" }}
|
|
77
95
|
</v-list-item>
|
|
78
96
|
</v-list>
|
|
79
97
|
</v-menu>
|
|
@@ -85,6 +103,34 @@
|
|
|
85
103
|
</AppCard>
|
|
86
104
|
</v-col>
|
|
87
105
|
|
|
106
|
+
<v-dialog :model-value="!!detailItem" max-width="520" @update:model-value="detailItem = null">
|
|
107
|
+
<v-card v-if="detailItem" class="pa-4">
|
|
108
|
+
<div class="text-h6 mb-1">{{ siteLabel(detailItem) }}</div>
|
|
109
|
+
<div class="text-caption mb-3">
|
|
110
|
+
{{ detailItem.email }} - one invitation e-mail; accepting it adds every site below.
|
|
111
|
+
</div>
|
|
112
|
+
<v-list density="compact" style="max-height: 50vh; overflow-y: auto">
|
|
113
|
+
<v-list-item v-for="s in detailItem.sites" :key="s._id" :title="s.siteName || 'Site'">
|
|
114
|
+
<template #append>
|
|
115
|
+
<span class="text-caption me-3">{{ formatStatus(s.status || "") }}</span>
|
|
116
|
+
<v-btn
|
|
117
|
+
v-if="s.status === 'pending' && props.cancelInvitation"
|
|
118
|
+
size="small"
|
|
119
|
+
variant="text"
|
|
120
|
+
color="error"
|
|
121
|
+
@click="openConfirmDialog([s._id])"
|
|
122
|
+
>
|
|
123
|
+
Cancel
|
|
124
|
+
</v-btn>
|
|
125
|
+
</template>
|
|
126
|
+
</v-list-item>
|
|
127
|
+
</v-list>
|
|
128
|
+
<div class="d-flex justify-end mt-3">
|
|
129
|
+
<v-btn variant="text" @click="detailItem = null">Close</v-btn>
|
|
130
|
+
</div>
|
|
131
|
+
</v-card>
|
|
132
|
+
</v-dialog>
|
|
133
|
+
|
|
88
134
|
<ConfirmDialog
|
|
89
135
|
v-model="confirmDialog"
|
|
90
136
|
:loading="cancelLoading"
|
|
@@ -95,8 +141,12 @@
|
|
|
95
141
|
</template>
|
|
96
142
|
<template #description>
|
|
97
143
|
<p class="text-subtitle-2">
|
|
98
|
-
|
|
99
|
-
|
|
144
|
+
{{
|
|
145
|
+
selectedInviteIds.length > 1
|
|
146
|
+
? `Are you sure you want to cancel all ${selectedInviteIds.length} site invitations?`
|
|
147
|
+
: "Are you sure you want to cancel this invitation?"
|
|
148
|
+
}}
|
|
149
|
+
This action cannot be undone.
|
|
100
150
|
</p>
|
|
101
151
|
</template>
|
|
102
152
|
<template #footer>
|
|
@@ -140,6 +190,13 @@
|
|
|
140
190
|
<script setup lang="ts">
|
|
141
191
|
// Relative import: this layer's `utils/` is not auto-imported into consumers.
|
|
142
192
|
import { invitationsEmptyText } from "../utils/invitations-empty-text";
|
|
193
|
+
import {
|
|
194
|
+
groupIds,
|
|
195
|
+
resendArgs,
|
|
196
|
+
siteCountOf,
|
|
197
|
+
siteLabel,
|
|
198
|
+
} from "../utils/invite-group";
|
|
199
|
+
import useUser from "../composables/useUser";
|
|
143
200
|
|
|
144
201
|
/** The same two tabs, same labels, same routes. */
|
|
145
202
|
const invitationTabs = [
|
|
@@ -205,7 +262,8 @@ const headers = computed(() => {
|
|
|
205
262
|
},
|
|
206
263
|
{
|
|
207
264
|
title: "Site Invited To",
|
|
208
|
-
value: "
|
|
265
|
+
value: "siteLabel",
|
|
266
|
+
sortable: false,
|
|
209
267
|
},
|
|
210
268
|
{
|
|
211
269
|
title: "Status",
|
|
@@ -214,7 +272,7 @@ const headers = computed(() => {
|
|
|
214
272
|
|
|
215
273
|
];
|
|
216
274
|
|
|
217
|
-
if (props.status === "pending" && props.cancelInvitation) {
|
|
275
|
+
if (props.status === "pending" && (props.cancelInvitation || props.inviteMember)) {
|
|
218
276
|
baseHeaders.push({
|
|
219
277
|
title: "Action",
|
|
220
278
|
value: "action-table",
|
|
@@ -226,6 +284,7 @@ const headers = computed(() => {
|
|
|
226
284
|
|
|
227
285
|
const { getVerifications: _getVerifications, cancelUserInvitation } =
|
|
228
286
|
useVerification();
|
|
287
|
+
const { inviteMemberClient } = useUser();
|
|
229
288
|
|
|
230
289
|
const page = ref(1);
|
|
231
290
|
const pages = ref(0);
|
|
@@ -255,20 +314,31 @@ const {
|
|
|
255
314
|
pageRange: string;
|
|
256
315
|
}>(
|
|
257
316
|
`get-verifications-${props.app}-${props.org}-${props.siteId}-${props.status}`,
|
|
258
|
-
() =>
|
|
259
|
-
|
|
317
|
+
async () => {
|
|
318
|
+
const query = {
|
|
260
319
|
page: page.value,
|
|
261
320
|
status: props.status,
|
|
262
321
|
search: headerSearch.value,
|
|
263
322
|
// type: "user-invite,member-invite",
|
|
264
323
|
type: "member-invite",
|
|
265
|
-
|
|
324
|
+
// The org console (organization app, no site) lists every invitation of
|
|
325
|
+
// the organisation, including the site invitations sent from there
|
|
326
|
+
// (owner request 2026-10-02); the server's list scoping is unchanged.
|
|
327
|
+
app: props.app === "organization" && !props.siteId ? "" : props.app,
|
|
266
328
|
orgId: props.org,
|
|
267
329
|
// A site app lists only THIS site's invitations; the server checks the
|
|
268
330
|
// caller reaches it (owner report 2026-09-28: LakeGarden's invitations
|
|
269
331
|
// showed on every Wing Tai site). The org console passes no site.
|
|
270
332
|
siteId: props.siteId,
|
|
271
|
-
}
|
|
333
|
+
};
|
|
334
|
+
// One row per multi-site invitation. An API without `group` refuses the
|
|
335
|
+
// key: fall back to the plain list (one row per site, as before).
|
|
336
|
+
try {
|
|
337
|
+
return await _getVerifications({ ...query, group: true });
|
|
338
|
+
} catch {
|
|
339
|
+
return await _getVerifications(query);
|
|
340
|
+
}
|
|
341
|
+
},
|
|
272
342
|
{
|
|
273
343
|
watch: [() => props.status]
|
|
274
344
|
}
|
|
@@ -306,31 +376,63 @@ watch([headerSearch, () => props.status], () => {
|
|
|
306
376
|
});
|
|
307
377
|
|
|
308
378
|
const confirmDialog = ref(false);
|
|
309
|
-
const
|
|
379
|
+
const selectedInviteIds = ref<string[]>([]);
|
|
310
380
|
const cancelLoading = ref(false);
|
|
381
|
+
const detailItem = ref<any>(null);
|
|
382
|
+
const resending = ref(false);
|
|
311
383
|
|
|
312
|
-
function openConfirmDialog(
|
|
313
|
-
|
|
384
|
+
function openConfirmDialog(ids: string[]) {
|
|
385
|
+
selectedInviteIds.value = ids;
|
|
314
386
|
confirmDialog.value = true;
|
|
315
387
|
}
|
|
316
388
|
|
|
317
389
|
async function onConfirmCancel() {
|
|
318
|
-
if (!
|
|
390
|
+
if (!selectedInviteIds.value.length) return;
|
|
319
391
|
|
|
320
392
|
cancelLoading.value = true;
|
|
321
393
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
394
|
+
let res: Record<string, any> = {};
|
|
395
|
+
for (const id of selectedInviteIds.value) {
|
|
396
|
+
res = await cancelUserInvitation(id);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
showMessage(
|
|
400
|
+
selectedInviteIds.value.length > 1
|
|
401
|
+
? "All site invitations cancelled"
|
|
402
|
+
: res.message || "Invitation cancelled successfully",
|
|
403
|
+
"success",
|
|
404
|
+
);
|
|
405
|
+
detailItem.value = null;
|
|
325
406
|
await getVerifications();
|
|
326
407
|
confirmDialog.value = false;
|
|
327
|
-
|
|
408
|
+
selectedInviteIds.value = [];
|
|
328
409
|
} catch (error: any) {
|
|
329
|
-
showMessage(error?.message || "Failed to cancel invitation", "error");
|
|
410
|
+
showMessage(error?.data?.message || error?.message || "Failed to cancel invitation", "error");
|
|
411
|
+
await getVerifications();
|
|
330
412
|
}
|
|
331
413
|
cancelLoading.value = false;
|
|
332
414
|
}
|
|
333
415
|
|
|
416
|
+
// ONE request = ONE e-mail for the whole group (the server re-uses the
|
|
417
|
+
// pending rows and names every site).
|
|
418
|
+
async function onResend(item: any) {
|
|
419
|
+
const args = resendArgs(item);
|
|
420
|
+
if (!args) {
|
|
421
|
+
showMessage("This invitation cannot be re-sent from here.", "error");
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (resending.value) return;
|
|
425
|
+
resending.value = true;
|
|
426
|
+
try {
|
|
427
|
+
await inviteMemberClient(args);
|
|
428
|
+
showMessage("Invitation re-sent (one e-mail).", "success");
|
|
429
|
+
await getVerifications();
|
|
430
|
+
} catch (error: any) {
|
|
431
|
+
showMessage(error?.data?.message || error?.message || "Failed to resend invitation", "error");
|
|
432
|
+
}
|
|
433
|
+
resending.value = false;
|
|
434
|
+
}
|
|
435
|
+
|
|
334
436
|
function handleSuccess(closeForm: boolean) {
|
|
335
437
|
getVerifications();
|
|
336
438
|
if (closeForm) {
|
package/composables/useUser.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inviteSitePayload } from "../utils/invite-sites";
|
|
2
|
+
|
|
1
3
|
export default function useUser() {
|
|
2
4
|
function inviteUser({
|
|
3
5
|
email = "",
|
|
@@ -65,7 +67,10 @@ export default function useUser() {
|
|
|
65
67
|
org = "",
|
|
66
68
|
site = "",
|
|
67
69
|
siteName = "",
|
|
70
|
+
sites = [] as string[],
|
|
68
71
|
} = {}) {
|
|
72
|
+
// `sites` = the several chosen sites; the old single `site` still works.
|
|
73
|
+
const chosen = sites.length ? sites : site ? [site] : [];
|
|
69
74
|
return useNuxtApp().$api<Record<string, any>>("/api/auth/invite/member", {
|
|
70
75
|
method: "POST",
|
|
71
76
|
body: {
|
|
@@ -74,8 +79,7 @@ export default function useUser() {
|
|
|
74
79
|
role,
|
|
75
80
|
name,
|
|
76
81
|
org,
|
|
77
|
-
...(
|
|
78
|
-
...(siteName && { siteName }),
|
|
82
|
+
...inviteSitePayload(chosen, siteName),
|
|
79
83
|
},
|
|
80
84
|
});
|
|
81
85
|
}
|
|
@@ -11,6 +11,8 @@ export default function useVerification() {
|
|
|
11
11
|
limit = 20,
|
|
12
12
|
// "seven365": only the invitations Seven365 sent (the console's lists).
|
|
13
13
|
origin = "",
|
|
14
|
+
// true: one entry per multi-site invitation group, with its `sites`.
|
|
15
|
+
group = false,
|
|
14
16
|
} = {}): Promise<{
|
|
15
17
|
items: TMiniVerification[];
|
|
16
18
|
pages: number;
|
|
@@ -22,6 +24,7 @@ export default function useVerification() {
|
|
|
22
24
|
status, search, page, type, email, app, siteId, orgId, limit,
|
|
23
25
|
// Sent only when asked for: an older API refuses a key it does not know.
|
|
24
26
|
...(origin && { origin }),
|
|
27
|
+
...(group && { group: "true" }),
|
|
25
28
|
},
|
|
26
29
|
});
|
|
27
30
|
}
|
package/middleware/01.auth.ts
CHANGED
|
@@ -4,13 +4,9 @@ export default defineNuxtRouteMiddleware(async (route) => {
|
|
|
4
4
|
// Get access token from cookies
|
|
5
5
|
const sid = useCookie("sid", cookieConfig).value;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (serviceProviderEmail) {
|
|
11
|
-
useCookie("service-provider-email", cookieConfig).value =
|
|
12
|
-
serviceProviderEmail;
|
|
13
|
-
}
|
|
7
|
+
// No longer saves `?email=` as a browser-wide `service-provider-email` cookie
|
|
8
|
+
// (30 days): nothing reads it, and a leftover from another account sent the
|
|
9
|
+
// org app's sign-in to that account's create page.
|
|
14
10
|
|
|
15
11
|
if (!sid) {
|
|
16
12
|
// Redirect to login page if no access token
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.93.0",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invitations list, multi-site invitations (owner request 2026-10-02): the
|
|
3
|
+
* server (`GET /api/verifications?group=true`) returns ONE entry per invitation
|
|
4
|
+
* group with its `sites`; an old single-site invitation is a group of one.
|
|
5
|
+
*/
|
|
6
|
+
export type InviteSiteRow = {
|
|
7
|
+
_id: string;
|
|
8
|
+
siteId?: string;
|
|
9
|
+
siteName?: string;
|
|
10
|
+
status?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type InviteEntry = {
|
|
14
|
+
_id?: string;
|
|
15
|
+
email?: string;
|
|
16
|
+
metadata?: Record<string, any>;
|
|
17
|
+
sites?: InviteSiteRow[];
|
|
18
|
+
siteCount?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function siteCountOf(item: InviteEntry): number {
|
|
22
|
+
return item.siteCount ?? item.sites?.length ?? 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** "LakeGarden" for one site, "21 sites" for a group (details list them all). */
|
|
26
|
+
export function siteLabel(item: InviteEntry): string {
|
|
27
|
+
const n = siteCountOf(item);
|
|
28
|
+
if (n > 1) return `${n} sites`;
|
|
29
|
+
return item.sites?.[0]?.siteName || item.metadata?.siteName || "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Every verification id the entry stands for (Cancel all). */
|
|
33
|
+
export function groupIds(item: InviteEntry): string[] {
|
|
34
|
+
const ids = item.sites?.length
|
|
35
|
+
? item.sites.map((s) => s._id)
|
|
36
|
+
: [item._id ?? ""];
|
|
37
|
+
return ids.filter(Boolean).map(String);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Arguments of `inviteMemberClient` that re-send the invitation: ONE request,
|
|
42
|
+
* so ONE e-mail for the whole group. `null` when it cannot be rebuilt (no org).
|
|
43
|
+
*/
|
|
44
|
+
export function resendArgs(item: InviteEntry) {
|
|
45
|
+
const m = item.metadata ?? {};
|
|
46
|
+
if (!item.email || !m.org) return null;
|
|
47
|
+
const sites = (item.sites ?? [])
|
|
48
|
+
.map((s) => s.siteId)
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.map(String);
|
|
51
|
+
return {
|
|
52
|
+
email: item.email,
|
|
53
|
+
app: m.app ?? "",
|
|
54
|
+
role: m.role ? String(m.role) : "",
|
|
55
|
+
name: m.name ?? "",
|
|
56
|
+
org: String(m.org),
|
|
57
|
+
sites: sites.length > 1 ? sites : [],
|
|
58
|
+
site: sites.length === 1 ? sites[0] : m.siteId ? String(m.siteId) : "",
|
|
59
|
+
siteName: item.sites?.[0]?.siteName || m.siteName || "",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which sites an "Invite member" request names (owner request 2026-10-02:
|
|
3
|
+
* invite one person to several sites). One site keeps the old `siteId` shape;
|
|
4
|
+
* 2 or more send `siteIds` (the server writes one invitation per site and
|
|
5
|
+
* names every site in one e-mail).
|
|
6
|
+
*/
|
|
7
|
+
export function inviteSitePayload(
|
|
8
|
+
sites: string[],
|
|
9
|
+
siteName = "",
|
|
10
|
+
): Record<string, unknown> {
|
|
11
|
+
if (!sites.length) return {};
|
|
12
|
+
if (sites.length === 1) {
|
|
13
|
+
return { siteId: sites[0], ...(siteName && { siteName }) };
|
|
14
|
+
}
|
|
15
|
+
return { siteIds: sites };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The one site a role is looked up for: the single chosen site, or "" for 2+
|
|
20
|
+
* (a site role is valid at its own site only, so several sites offer
|
|
21
|
+
* organisation roles only).
|
|
22
|
+
*/
|
|
23
|
+
export function roleSiteFor(sites: string[]): string {
|
|
24
|
+
return sites.length === 1 ? sites[0] : "";
|
|
25
|
+
}
|