@7365admin1/layer-common 4.73.1-staging.454 → 4.73.1-staging.456
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/components/MemberMain.vue +68 -13
- package/composables/useMember.ts +4 -0
- package/package.json +1 -1
- package/utils/role.ts +43 -6
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
<div class="d-flex align-center flex-wrap ga-2">
|
|
42
42
|
<span class="flex-grow-1">{{ otherSiteBanner }}</span>
|
|
43
43
|
<AppButton
|
|
44
|
+
v-if="!orgLevel"
|
|
44
45
|
data-test="member-other-site-filter"
|
|
45
46
|
@click="toggleOtherSite()"
|
|
46
47
|
>
|
|
@@ -100,6 +101,8 @@
|
|
|
100
101
|
|
|
101
102
|
<div class="app-table-scroll">
|
|
102
103
|
<v-data-table
|
|
104
|
+
class="member-table"
|
|
105
|
+
:class="{ 'member-table--pin-action': actionLast }"
|
|
103
106
|
:headers="props.headers"
|
|
104
107
|
:items="items"
|
|
105
108
|
item-value="_id"
|
|
@@ -118,15 +121,21 @@
|
|
|
118
121
|
(memberRoleChip in utils/role.ts, server roleState). -->
|
|
119
122
|
<template #item.roleName="{ item }">
|
|
120
123
|
<StatusChip
|
|
121
|
-
v-if="memberRoleChip(item)"
|
|
124
|
+
v-if="memberRoleChip(item, orgLevel)"
|
|
122
125
|
:dot="false"
|
|
123
126
|
pill
|
|
124
|
-
:tone="memberRoleChip(item)?.tone"
|
|
127
|
+
:tone="memberRoleChip(item, orgLevel)?.tone"
|
|
128
|
+
class="member-role-chip"
|
|
125
129
|
data-test="member-role-chip"
|
|
126
130
|
>
|
|
127
|
-
{{ memberRoleChip(item)?.text }}
|
|
131
|
+
{{ memberRoleChip(item, orgLevel)?.text }}
|
|
128
132
|
</StatusChip>
|
|
129
133
|
</template>
|
|
134
|
+
<!-- Organization app: every person, and where they may go
|
|
135
|
+
(owner report 2026-09-29: a site-limited person stays listed). -->
|
|
136
|
+
<template v-if="orgLevel" #item.siteName="{ item }">
|
|
137
|
+
<span data-test="member-site-scope">{{ siteAccessLabel(item) }}</span>
|
|
138
|
+
</template>
|
|
130
139
|
<template #item.dateInvited="{ item }">
|
|
131
140
|
{{ formatDateInvited(item.dateInvited) }}
|
|
132
141
|
</template>
|
|
@@ -134,6 +143,18 @@
|
|
|
134
143
|
<span data-test="member-last-active">{{ formatLastActive(item.lastLoginAt) }}</span>
|
|
135
144
|
</template>
|
|
136
145
|
<template #item.action-table="{ item }">
|
|
146
|
+
<div class="d-flex align-center justify-end ga-1">
|
|
147
|
+
<!-- The main task on the organisation's Members list, so it is a
|
|
148
|
+
button on every row rather than only inside the menu. -->
|
|
149
|
+
<AppButton
|
|
150
|
+
v-if="siteAccessAllowed && item.user"
|
|
151
|
+
variant="ghost"
|
|
152
|
+
icon="mdi-map-marker-multiple-outline"
|
|
153
|
+
data-test="member-site-access-button"
|
|
154
|
+
@click="openSiteAccess(item)"
|
|
155
|
+
>
|
|
156
|
+
Site access
|
|
157
|
+
</AppButton>
|
|
137
158
|
<!--
|
|
138
159
|
`props.assignRole` belongs in this test. The menu's only item that
|
|
139
160
|
is not status-dependent is Assign Role, but the menu itself opened
|
|
@@ -142,12 +163,12 @@
|
|
|
142
163
|
and the permission was unreachable.
|
|
143
164
|
-->
|
|
144
165
|
<v-menu
|
|
145
|
-
v-if="
|
|
166
|
+
v-if="!isLimited(item) && (
|
|
146
167
|
props.assignRole ||
|
|
147
168
|
(currentStatus === 'active' &&
|
|
148
169
|
(canSuspendMembers || canDeleteMembers)) ||
|
|
149
170
|
(currentStatus === 'suspended' &&
|
|
150
|
-
(canActivateMembers || canDeleteMembers))
|
|
171
|
+
(canActivateMembers || canDeleteMembers)))
|
|
151
172
|
"
|
|
152
173
|
v-model="item.menuOpen"
|
|
153
174
|
:close-on-content-click="false"
|
|
@@ -216,6 +237,7 @@
|
|
|
216
237
|
</v-list-item>
|
|
217
238
|
</v-list>
|
|
218
239
|
</v-menu>
|
|
240
|
+
</div>
|
|
219
241
|
</template>
|
|
220
242
|
</v-data-table>
|
|
221
243
|
</div>
|
|
@@ -349,6 +371,8 @@ import {
|
|
|
349
371
|
otherSiteBannerText,
|
|
350
372
|
roleUsableHere,
|
|
351
373
|
safeReturnTo,
|
|
374
|
+
siteAccessLabel,
|
|
375
|
+
limitedSites,
|
|
352
376
|
withCurrentRole,
|
|
353
377
|
} from "../utils/role";
|
|
354
378
|
import { formatLastActive } from "../utils/last-active";
|
|
@@ -457,6 +481,11 @@ const props = defineProps({
|
|
|
457
481
|
},
|
|
458
482
|
});
|
|
459
483
|
|
|
484
|
+
/* The Organization app's Members list: no site, organisation memberships. It
|
|
485
|
+
lists every person (site-limited ones as one row with `limitedTo`), labels
|
|
486
|
+
where each may go, and has no "role from another site" banner - there is no
|
|
487
|
+
"this site" here. */
|
|
488
|
+
const orgLevel = computed(() => !props.siteId && props.type === "organization");
|
|
460
489
|
const items = ref<Array<Record<string, any>>>([]);
|
|
461
490
|
const onlyOtherSite = ref(false);
|
|
462
491
|
const page = ref(1);
|
|
@@ -533,6 +562,8 @@ const {
|
|
|
533
562
|
type: props.type,
|
|
534
563
|
siteId: props.siteId,
|
|
535
564
|
roleState: onlyOtherSite.value ? "other-site" : "",
|
|
565
|
+
// Organization app: site-limited people too, one row each (`limitedTo`).
|
|
566
|
+
people: orgLevel.value,
|
|
536
567
|
}),
|
|
537
568
|
{
|
|
538
569
|
watch: [() => props.status, () => props.orgId, () => props.siteId, headerSearch, onlyOtherSite],
|
|
@@ -561,8 +592,10 @@ const { data: otherSiteReq, refresh: refreshOtherSite } = useAsyncData(
|
|
|
561
592
|
: Promise.resolve(null),
|
|
562
593
|
{ watch: [() => props.status, () => props.orgId, () => props.siteId] }
|
|
563
594
|
);
|
|
595
|
+
// At organisation level there is no "this site": the count says what is wrong
|
|
596
|
+
// and points to "Fix roles from other sites" (no per-site filter there).
|
|
564
597
|
const otherSiteBanner = computed(() =>
|
|
565
|
-
otherSiteBannerText(otherSiteReq.value?.total)
|
|
598
|
+
otherSiteBannerText(otherSiteReq.value?.total, orgLevel.value)
|
|
566
599
|
);
|
|
567
600
|
watch(otherSiteBanner, (text) => {
|
|
568
601
|
// the last one fixed: back to the full list
|
|
@@ -632,6 +665,12 @@ function setMember({
|
|
|
632
665
|
screen, whoever may assign roles can choose a member's sites. The server
|
|
633
666
|
decides again (owner / org-wide admin only). */
|
|
634
667
|
const siteAccessAllowed = computed(() => props.assignRole && !props.siteId);
|
|
668
|
+
/* A site-limited person's row stands for several per-site memberships, so the
|
|
669
|
+
one-membership actions (Assign Role, Suspend, Delete) are not offered on it;
|
|
670
|
+
Site access manages them. */
|
|
671
|
+
const isLimited = (item: Record<string, any>) => orgLevel.value && limitedSites(item).length > 0;
|
|
672
|
+
/* Keep the actions column on screen however wide the table gets. */
|
|
673
|
+
const actionLast = computed(() => props.headers[props.headers.length - 1]?.value === "action-table");
|
|
635
674
|
const siteAccessDialog = ref(false);
|
|
636
675
|
const siteAccessUser = ref("");
|
|
637
676
|
const siteAccessName = ref("");
|
|
@@ -777,20 +816,16 @@ function formatDateInvited(value?: string | null) {
|
|
|
777
816
|
if (Number.isNaN(date.getTime())) return "";
|
|
778
817
|
|
|
779
818
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
780
|
-
month: "
|
|
819
|
+
month: "short",
|
|
781
820
|
day: "numeric",
|
|
782
821
|
year: "numeric",
|
|
783
|
-
hour: "2-digit",
|
|
784
|
-
minute: "2-digit",
|
|
785
|
-
hour12: true,
|
|
786
822
|
}).formatToParts(date);
|
|
787
823
|
|
|
788
824
|
const getPart = (type: Intl.DateTimeFormatPartTypes) =>
|
|
789
825
|
parts.find((part) => part.type === type)?.value ?? "";
|
|
790
826
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
)}:${getPart("minute")}${getPart("dayPeriod").toUpperCase()}`;
|
|
827
|
+
// Short ("Sep 29, 2026") so the table fits and the actions stay in view.
|
|
828
|
+
return `${getPart("month")} ${getPart("day")}, ${getPart("year")}`;
|
|
794
829
|
}
|
|
795
830
|
|
|
796
831
|
async function handleUpdateMemberStatus() {
|
|
@@ -826,3 +861,23 @@ const updateActionText = computed(() => {
|
|
|
826
861
|
return map[updateAction.value] || updateAction.value;
|
|
827
862
|
});
|
|
828
863
|
</script>
|
|
864
|
+
|
|
865
|
+
<style scoped>
|
|
866
|
+
/* The actions column stays pinned to the right edge (owner report 2026-09-29:
|
|
867
|
+
at 1151px the menu sat off-screen behind a sideways scroll). */
|
|
868
|
+
.member-table--pin-action :deep(th:last-child),
|
|
869
|
+
.member-table--pin-action :deep(td:last-child) {
|
|
870
|
+
position: sticky;
|
|
871
|
+
right: 0;
|
|
872
|
+
z-index: 1;
|
|
873
|
+
background: rgb(var(--v-theme-surface));
|
|
874
|
+
box-shadow: -1px 0 0 rgba(var(--v-border-color), var(--v-border-opacity));
|
|
875
|
+
}
|
|
876
|
+
.member-table--pin-action :deep(thead th:last-child) {
|
|
877
|
+
z-index: 3;
|
|
878
|
+
}
|
|
879
|
+
.member-table :deep(.member-role-chip) {
|
|
880
|
+
white-space: normal;
|
|
881
|
+
height: auto;
|
|
882
|
+
}
|
|
883
|
+
</style>
|
package/composables/useMember.ts
CHANGED
|
@@ -72,9 +72,13 @@ export default function useMember() {
|
|
|
72
72
|
roleState = "",
|
|
73
73
|
/** A building unit id, or "none": members on no unit yet. */
|
|
74
74
|
unit = "",
|
|
75
|
+
people = false,
|
|
75
76
|
} = {}) {
|
|
76
77
|
const query: Record<string, any> = { page, limit, type, status };
|
|
77
78
|
|
|
79
|
+
// Organization app Members: every person, site-limited ones as one row
|
|
80
|
+
// with `limitedTo` (core 2026-09-29). Only sent when asked for.
|
|
81
|
+
if (people) query.people = 1;
|
|
78
82
|
if (search) query.search = search;
|
|
79
83
|
// "other-site": only members holding a role pinned to another site.
|
|
80
84
|
if (roleState) query.roleState = roleState;
|
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.73.1-staging.
|
|
5
|
+
"version": "4.73.1-staging.456",
|
|
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.",
|
package/utils/role.ts
CHANGED
|
@@ -36,12 +36,28 @@ export function withCurrentRole<T extends { _id?: unknown; name?: unknown }>(
|
|
|
36
36
|
* a member showed "Security" and Assign Role could not fix it). Rows from an
|
|
37
37
|
* older server carry no `roleState` and render exactly as before.
|
|
38
38
|
*/
|
|
39
|
-
export function memberRoleChip(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
export function memberRoleChip(
|
|
40
|
+
item: {
|
|
41
|
+
roleName?: unknown;
|
|
42
|
+
roleState?: unknown;
|
|
43
|
+
roleSiteName?: unknown;
|
|
44
|
+
limitedTo?: unknown;
|
|
45
|
+
},
|
|
46
|
+
/** The Organization app's Members list: there is no "this site" there. */
|
|
47
|
+
orgLevel = false,
|
|
48
|
+
): { text: string; tone: "info" | "warn" } | null {
|
|
44
49
|
const name = typeof item?.roleName === "string" ? item.roleName : "";
|
|
50
|
+
const limited = limitedSites(item);
|
|
51
|
+
if (orgLevel && limited.length) {
|
|
52
|
+
return {
|
|
53
|
+
text: limited.map((l) => `${l.roleName || "No role"} (${l.siteName || "a site"})`).join(", "),
|
|
54
|
+
tone: "info",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (orgLevel && item?.roleState === "other-site") {
|
|
58
|
+
const site = typeof item?.roleSiteName === "string" && item.roleSiteName ? item.roleSiteName : "";
|
|
59
|
+
return { text: site ? `${name || "Role"} (${site})` : name || "Role", tone: "info" };
|
|
60
|
+
}
|
|
45
61
|
switch (item?.roleState) {
|
|
46
62
|
case "missing":
|
|
47
63
|
return { text: "No role assigned - assign one", tone: "warn" };
|
|
@@ -66,9 +82,14 @@ export function memberRoleChip(item: {
|
|
|
66
82
|
* re-assign a role for the right site, nobody's access is cut). `null` at 0 so
|
|
67
83
|
* the banner disappears once every member here has this site's role.
|
|
68
84
|
*/
|
|
69
|
-
export function otherSiteBannerText(count: unknown): string | null {
|
|
85
|
+
export function otherSiteBannerText(count: unknown, orgLevel = false): string | null {
|
|
70
86
|
const n = typeof count === "number" && Number.isFinite(count) ? Math.floor(count) : 0;
|
|
71
87
|
if (n <= 0) return null;
|
|
88
|
+
if (orgLevel) {
|
|
89
|
+
return n === 1
|
|
90
|
+
? "1 member holds a role created for one particular site. Fix roles from other sites gives them the right role."
|
|
91
|
+
: `${n} members hold a role created for one particular site. Fix roles from other sites gives each one the right role.`;
|
|
92
|
+
}
|
|
72
93
|
return n === 1
|
|
73
94
|
? "1 member here has a role from another site. Assign them a role for this site."
|
|
74
95
|
: `${n} members here have a role from another site. Assign each one a role for this site.`;
|
|
@@ -95,3 +116,19 @@ export function safeReturnTo(value: unknown): string {
|
|
|
95
116
|
const path = Array.isArray(value) ? value[0] : value;
|
|
96
117
|
return typeof path === "string" && /^\/(?!\/)[^\s\\]*$/.test(path) ? path : "";
|
|
97
118
|
}
|
|
119
|
+
|
|
120
|
+
type TLimitedSite = { siteId?: string; siteName?: string; roleName?: string };
|
|
121
|
+
|
|
122
|
+
/** The sites a site-limited person may open (server `limitedTo`; org Members). */
|
|
123
|
+
export function limitedSites(item: { limitedTo?: unknown } | null | undefined): TLimitedSite[] {
|
|
124
|
+
return Array.isArray(item?.limitedTo) ? (item!.limitedTo as TLimitedSite[]) : [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The Organization app's Members "Sites" cell (owner report 2026-09-29, Aigle
|
|
129
|
+
* / Kanesan): a person limited to sites stays on the list and says where.
|
|
130
|
+
*/
|
|
131
|
+
export function siteAccessLabel(item: { limitedTo?: unknown } | null | undefined): string {
|
|
132
|
+
const names = limitedSites(item).map((l) => l.siteName || "a site");
|
|
133
|
+
return names.length ? `Limited to: ${names.join(", ")}` : "All sites";
|
|
134
|
+
}
|