@7365admin1/layer-common 4.91.0 → 4.92.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,15 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.92.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9883bfd: Invite member: pick several sites in one invitation.
8
+
9
+ - `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.
10
+ - `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`.
11
+ - `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.
12
+
3
13
  ## 4.91.0
4
14
 
5
15
  ### Minor Changes
@@ -2034,11 +2044,11 @@
2034
2044
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
2035
2045
  own rule from two endpoints the console already calls, unprojected:
2036
2046
 
2037
- GET /api/members/user/:user/app/admin the Seven365 staff membership
2038
- GET /api/roles/id/:role that membership's role document
2047
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
2048
+ GET /api/roles/id/:role that membership's role document
2039
2049
 
2040
- owner = member.type === "admin" && role.type === "admin" && role.default === true
2041
- staff = member.type === "admin" && role.type === "admin"
2050
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
2051
+ staff = member.type === "admin" && role.type === "admin"
2042
2052
 
2043
2053
  `role.default` is the marker because it is the only property of a platform
2044
2054
  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.site"
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="[requiredRule]"
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(siteId: string) {
552
- const obj = sites.value.find((x) => x?.value === siteId);
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
 
@@ -262,7 +262,10 @@ const {
262
262
  search: headerSearch.value,
263
263
  // type: "user-invite,member-invite",
264
264
  type: "member-invite",
265
- app: props.app,
265
+ // The org console (organization app, no site) lists every invitation of
266
+ // the organisation, including the site invitations sent from there
267
+ // (owner request 2026-10-02); the server's list scoping is unchanged.
268
+ app: props.app === "organization" && !props.siteId ? "" : props.app,
266
269
  orgId: props.org,
267
270
  // A site app lists only THIS site's invitations; the server checks the
268
271
  // caller reaches it (owner report 2026-09-28: LakeGarden's invitations
@@ -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
- ...(site && { siteId: site }),
78
- ...(siteName && { siteName }),
82
+ ...inviteSitePayload(chosen, siteName),
79
83
  },
80
84
  });
81
85
  }
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.91.0",
5
+ "version": "4.92.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,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
+ }