@7365admin1/layer-common 4.35.1-staging.429 → 4.35.1-staging.431
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.
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
hide-default-footer
|
|
51
51
|
style="max-height: calc(100vh - (126px))"
|
|
52
52
|
:loading="loading"
|
|
53
|
+
:no-data-text="emptyText"
|
|
53
54
|
>
|
|
54
55
|
<template #item.permissions="{ value }">
|
|
55
56
|
<span class="text-caption font-weight-bold text-capitalize">
|
|
@@ -137,6 +138,9 @@
|
|
|
137
138
|
</template>
|
|
138
139
|
|
|
139
140
|
<script setup lang="ts">
|
|
141
|
+
// Relative import: this layer's `utils/` is not auto-imported into consumers.
|
|
142
|
+
import { invitationsEmptyText } from "../utils/invitations-empty-text";
|
|
143
|
+
|
|
140
144
|
/** The same two tabs, same labels, same routes. */
|
|
141
145
|
const invitationTabs = [
|
|
142
146
|
{ status: "pending", label: "Pending" },
|
|
@@ -260,7 +264,10 @@ const {
|
|
|
260
264
|
type: "member-invite",
|
|
261
265
|
app: props.app,
|
|
262
266
|
orgId: props.org,
|
|
263
|
-
//
|
|
267
|
+
// A site app lists only THIS site's invitations; the server checks the
|
|
268
|
+
// caller reaches it (owner report 2026-09-28: LakeGarden's invitations
|
|
269
|
+
// showed on every Wing Tai site). The org console passes no site.
|
|
270
|
+
siteId: props.siteId,
|
|
264
271
|
}),
|
|
265
272
|
{
|
|
266
273
|
watch: [() => props.status]
|
|
@@ -269,6 +276,22 @@ const {
|
|
|
269
276
|
|
|
270
277
|
const loading = computed(() => getVerificationsStatus.value === "pending");
|
|
271
278
|
|
|
279
|
+
// A plain empty state instead of "No data available" (QA sweep 2026-09-28).
|
|
280
|
+
const siteName = ref("");
|
|
281
|
+
if (props.siteId) {
|
|
282
|
+
useSite()
|
|
283
|
+
.getSiteById(props.siteId)
|
|
284
|
+
.then((site) => (siteName.value = site?.name ?? ""))
|
|
285
|
+
.catch(() => {});
|
|
286
|
+
}
|
|
287
|
+
const emptyText = computed(() =>
|
|
288
|
+
invitationsEmptyText({
|
|
289
|
+
status: props.status,
|
|
290
|
+
siteName: props.siteId ? siteName.value || "this site" : "",
|
|
291
|
+
canInvite: props.inviteMember,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
|
|
272
295
|
watchEffect(() => {
|
|
273
296
|
if (getVerificationsReq.value) {
|
|
274
297
|
items.value = getVerificationsReq.value.items;
|
|
@@ -250,7 +250,11 @@
|
|
|
250
250
|
<v-col cols="12">
|
|
251
251
|
<v-row no-gutters>
|
|
252
252
|
<InputLabel class="text-capitalize" title="Name" required />
|
|
253
|
-
<v-col cols="12">
|
|
253
|
+
<v-col v-if="noSiteRoles" cols="12" class="py-2" data-test="assign-role-empty">
|
|
254
|
+
<p class="text-body-2 mb-2">{{ noSiteRolesText }}</p>
|
|
255
|
+
<NuxtLink v-if="rolesPageLink" :to="rolesPageLink">Go to Roles & Permissions</NuxtLink>
|
|
256
|
+
</v-col>
|
|
257
|
+
<v-col v-else cols="12">
|
|
254
258
|
<v-select
|
|
255
259
|
v-model="selectedRole"
|
|
256
260
|
:items="pickerRoles"
|
|
@@ -287,7 +291,7 @@
|
|
|
287
291
|
<v-btn
|
|
288
292
|
class="screen-btn-primary"
|
|
289
293
|
variant="flat"
|
|
290
|
-
:disabled="!memberRoleForm"
|
|
294
|
+
:disabled="!memberRoleForm || noSiteRoles"
|
|
291
295
|
type="submit"
|
|
292
296
|
>
|
|
293
297
|
Submit
|
|
@@ -300,7 +304,14 @@
|
|
|
300
304
|
</template>
|
|
301
305
|
|
|
302
306
|
<script setup lang="ts">
|
|
303
|
-
import {
|
|
307
|
+
import {
|
|
308
|
+
memberRoleChip,
|
|
309
|
+
noRolesForSiteText,
|
|
310
|
+
otherSiteBannerText,
|
|
311
|
+
roleUsableHere,
|
|
312
|
+
safeReturnTo,
|
|
313
|
+
withCurrentRole,
|
|
314
|
+
} from "../utils/role";
|
|
304
315
|
|
|
305
316
|
const props = defineProps({
|
|
306
317
|
/** The design's page title. Every app that mounts this screen calls it
|
|
@@ -547,9 +558,14 @@ function setMember({
|
|
|
547
558
|
}
|
|
548
559
|
|
|
549
560
|
selectedMemberId.value = role._id;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
561
|
+
// A role the member cannot use HERE (another site's, another organisation's,
|
|
562
|
+
// deleted, none) is neither pre-filled nor offered (owner report 2026-09-28,
|
|
563
|
+
// One Pearl Bank: The Loft's "Security" appeared as if created for this site).
|
|
564
|
+
const usable = roleUsableHere(role);
|
|
565
|
+
selectedRole.value = usable ? role.role : null;
|
|
566
|
+
selectedMemberRole.value = usable ? role.role : "";
|
|
567
|
+
selectedMemberRoleName.value = usable ? role.roleName ?? "" : "";
|
|
568
|
+
if (dialog) refreshRoles();
|
|
553
569
|
}
|
|
554
570
|
|
|
555
571
|
const roles = ref<Array<Record<string, any>>>([]);
|
|
@@ -593,7 +609,7 @@ const pickerRoles = computed(() =>
|
|
|
593
609
|
* filter would be decorative -- the same trap the server's own Redis key had.
|
|
594
610
|
*/
|
|
595
611
|
const { getRoles } = useRole();
|
|
596
|
-
const { data: getAllRoleReq } = useLazyAsyncData(
|
|
612
|
+
const { data: getAllRoleReq, refresh: refreshRoles } = useLazyAsyncData(
|
|
597
613
|
`get-roles-${props.type}-${props.orgId}-${props.siteId}`,
|
|
598
614
|
() =>
|
|
599
615
|
getRoles({
|
|
@@ -610,6 +626,33 @@ watchEffect(() => {
|
|
|
610
626
|
}
|
|
611
627
|
});
|
|
612
628
|
|
|
629
|
+
/* No role exists for this site yet: say so and point at Roles & Permissions,
|
|
630
|
+
which sends the user back here after creating one (`returnTo`). */
|
|
631
|
+
const noSiteRoles = computed(
|
|
632
|
+
() => !!props.siteId && !!getAllRoleReq.value && pickerRoles.value.length === 0
|
|
633
|
+
);
|
|
634
|
+
const assignSiteName = ref("");
|
|
635
|
+
if (props.siteId) {
|
|
636
|
+
useSite()
|
|
637
|
+
.getSiteById(props.siteId)
|
|
638
|
+
.then((site) => (assignSiteName.value = site?.name ?? ""))
|
|
639
|
+
.catch(() => {});
|
|
640
|
+
}
|
|
641
|
+
const noSiteRolesText = computed(() => noRolesForSiteText(assignSiteName.value));
|
|
642
|
+
const memberRoute = useRoute();
|
|
643
|
+
const rolesPageLink = computed(() => {
|
|
644
|
+
const here = memberRoute.path;
|
|
645
|
+
const path = here.replace(/\/members(\/.*)?$/, "/roles-permissions");
|
|
646
|
+
if (path === here) return "";
|
|
647
|
+
try {
|
|
648
|
+
return useRouter().resolve(path).matched.length
|
|
649
|
+
? `${path}?returnTo=${encodeURIComponent(here)}`
|
|
650
|
+
: "";
|
|
651
|
+
} catch {
|
|
652
|
+
return "";
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
613
656
|
watchEffect(() => {
|
|
614
657
|
if (selectedRole.value) {
|
|
615
658
|
message.value = "";
|
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
|
|
205
205
|
<script setup lang="ts">
|
|
206
206
|
// Relative import: this layer's `utils/` is not auto-imported into consumers.
|
|
207
|
-
import { showSiteField } from "../utils/role";
|
|
207
|
+
import { safeReturnTo, showSiteField } from "../utils/role";
|
|
208
208
|
import { holdsPermission } from "../utils/permission-spellings";
|
|
209
209
|
import {
|
|
210
210
|
catalogueForOrg,
|
|
@@ -509,6 +509,10 @@ const siteFieldShown = computed(() =>
|
|
|
509
509
|
function success() {
|
|
510
510
|
createDialog.value = false;
|
|
511
511
|
getRoles();
|
|
512
|
+
// Came here from Members -> Assign Role with no role for the site: go back to
|
|
513
|
+
// finish assigning (same-app path only).
|
|
514
|
+
const back = safeReturnTo(useRoute().query.returnTo);
|
|
515
|
+
if (back) navigateTo(back);
|
|
512
516
|
}
|
|
513
517
|
|
|
514
518
|
const previewDialog = ref(false);
|
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.35.1-staging.
|
|
5
|
+
"version": "4.35.1-staging.431",
|
|
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,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Invitations table's empty state (QA sweep 2026-09-28), e.g.
|
|
3
|
+
* "No pending invitations for The LakeGarden Residences. Use Invite member to
|
|
4
|
+
* add someone." / "No expired invitations."
|
|
5
|
+
*/
|
|
6
|
+
export function invitationsEmptyText({
|
|
7
|
+
status = "pending",
|
|
8
|
+
siteName = "",
|
|
9
|
+
canInvite = true,
|
|
10
|
+
}: { status?: string; siteName?: string; canInvite?: boolean } = {}): string {
|
|
11
|
+
if (status === "expired") return "No expired invitations.";
|
|
12
|
+
const where = siteName ? ` for ${siteName}` : "";
|
|
13
|
+
return `No pending invitations${where}.${canInvite ? " Use Invite member to add someone." : ""}`;
|
|
14
|
+
}
|
package/utils/role.ts
CHANGED
|
@@ -73,3 +73,24 @@ export function otherSiteBannerText(count: unknown): string | null {
|
|
|
73
73
|
? "1 member here has a role from another site. Assign them a role for this site."
|
|
74
74
|
: `${n} members here have a role from another site. Assign each one a role for this site.`;
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Can this member's CURRENT role be used on the screen's site? The server's
|
|
79
|
+
* `roleState` is empty for a usable role and names the problem otherwise
|
|
80
|
+
* ("other-site", "other-org", "deleted", "missing"). An unusable role is never
|
|
81
|
+
* pre-filled in Assign Role nor offered in its list (owner report 2026-09-28).
|
|
82
|
+
*/
|
|
83
|
+
export function roleUsableHere(member: { role?: unknown; roleState?: unknown }): boolean {
|
|
84
|
+
return !!member?.role && !member?.roleState;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Assign Role's empty state when the site has no role of its own yet. */
|
|
88
|
+
export function noRolesForSiteText(siteName?: string | null): string {
|
|
89
|
+
return `No roles created for ${siteName || "this site"} yet. Create one in Roles & Permissions.`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A same-app path to return to (`?returnTo=`), or "" - never another origin. */
|
|
93
|
+
export function safeReturnTo(value: unknown): string {
|
|
94
|
+
const path = Array.isArray(value) ? value[0] : value;
|
|
95
|
+
return typeof path === "string" && /^\/(?!\/)[^\s\\]*$/.test(path) ? path : "";
|
|
96
|
+
}
|