@7365admin1/layer-common 4.74.0 → 4.75.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 +10 -4
- package/components/MemberMain.vue +66 -14
- package/composables/useMember.ts +4 -0
- package/package.json +1 -1
- package/utils/role.ts +37 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.75.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- aa8c658: Organization app Members: lists every person of the organisation, including people limited to specific sites (one row each, labelled "Limited to: Site A, Site B"; organisation-wide people read "All sites"); a Site access button on every row; the actions column stays pinned on the right; shorter dates; at organisation level the "role from another site" banner is gone and such a role reads "Security (The Loft)". Needs core with `GET /members?people=1` (older servers refuse the flag).
|
|
8
|
+
|
|
3
9
|
## 4.74.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -1896,11 +1902,11 @@
|
|
|
1896
1902
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1897
1903
|
own rule from two endpoints the console already calls, unprojected:
|
|
1898
1904
|
|
|
1899
|
-
|
|
1900
|
-
|
|
1905
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1906
|
+
GET /api/roles/id/:role that membership's role document
|
|
1901
1907
|
|
|
1902
|
-
|
|
1903
|
-
|
|
1908
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1909
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1904
1910
|
|
|
1905
1911
|
`role.default` is the marker because it is the only property of a platform
|
|
1906
1912
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
their access; the company assigns each one a role for this site.
|
|
32
32
|
Shown while any remain (server count), gone at 0. -->
|
|
33
33
|
<v-alert
|
|
34
|
-
v-if="props.assignRole && otherSiteBanner"
|
|
34
|
+
v-if="props.assignRole && otherSiteBanner && !orgLevel"
|
|
35
35
|
type="warning"
|
|
36
36
|
variant="tonal"
|
|
37
37
|
density="compact"
|
|
@@ -100,6 +100,8 @@
|
|
|
100
100
|
|
|
101
101
|
<div class="app-table-scroll">
|
|
102
102
|
<v-data-table
|
|
103
|
+
class="member-table"
|
|
104
|
+
:class="{ 'member-table--pin-action': actionLast }"
|
|
103
105
|
:headers="props.headers"
|
|
104
106
|
:items="items"
|
|
105
107
|
item-value="_id"
|
|
@@ -118,15 +120,21 @@
|
|
|
118
120
|
(memberRoleChip in utils/role.ts, server roleState). -->
|
|
119
121
|
<template #item.roleName="{ item }">
|
|
120
122
|
<StatusChip
|
|
121
|
-
v-if="memberRoleChip(item)"
|
|
123
|
+
v-if="memberRoleChip(item, orgLevel)"
|
|
122
124
|
:dot="false"
|
|
123
125
|
pill
|
|
124
|
-
:tone="memberRoleChip(item)?.tone"
|
|
126
|
+
:tone="memberRoleChip(item, orgLevel)?.tone"
|
|
127
|
+
class="member-role-chip"
|
|
125
128
|
data-test="member-role-chip"
|
|
126
129
|
>
|
|
127
|
-
{{ memberRoleChip(item)?.text }}
|
|
130
|
+
{{ memberRoleChip(item, orgLevel)?.text }}
|
|
128
131
|
</StatusChip>
|
|
129
132
|
</template>
|
|
133
|
+
<!-- Organization app: every person, and where they may go
|
|
134
|
+
(owner report 2026-09-29: a site-limited person stays listed). -->
|
|
135
|
+
<template v-if="orgLevel" #item.siteName="{ item }">
|
|
136
|
+
<span data-test="member-site-scope">{{ siteAccessLabel(item) }}</span>
|
|
137
|
+
</template>
|
|
130
138
|
<template #item.dateInvited="{ item }">
|
|
131
139
|
{{ formatDateInvited(item.dateInvited) }}
|
|
132
140
|
</template>
|
|
@@ -134,6 +142,18 @@
|
|
|
134
142
|
<span data-test="member-last-active">{{ formatLastActive(item.lastLoginAt) }}</span>
|
|
135
143
|
</template>
|
|
136
144
|
<template #item.action-table="{ item }">
|
|
145
|
+
<div class="d-flex align-center justify-end ga-1">
|
|
146
|
+
<!-- The main task on the organisation's Members list, so it is a
|
|
147
|
+
button on every row rather than only inside the menu. -->
|
|
148
|
+
<AppButton
|
|
149
|
+
v-if="siteAccessAllowed && item.user"
|
|
150
|
+
variant="ghost"
|
|
151
|
+
icon="mdi-map-marker-multiple-outline"
|
|
152
|
+
data-test="member-site-access-button"
|
|
153
|
+
@click="openSiteAccess(item)"
|
|
154
|
+
>
|
|
155
|
+
Site access
|
|
156
|
+
</AppButton>
|
|
137
157
|
<!--
|
|
138
158
|
`props.assignRole` belongs in this test. The menu's only item that
|
|
139
159
|
is not status-dependent is Assign Role, but the menu itself opened
|
|
@@ -142,12 +162,12 @@
|
|
|
142
162
|
and the permission was unreachable.
|
|
143
163
|
-->
|
|
144
164
|
<v-menu
|
|
145
|
-
v-if="
|
|
165
|
+
v-if="!isLimited(item) && (
|
|
146
166
|
props.assignRole ||
|
|
147
167
|
(currentStatus === 'active' &&
|
|
148
168
|
(canSuspendMembers || canDeleteMembers)) ||
|
|
149
169
|
(currentStatus === 'suspended' &&
|
|
150
|
-
(canActivateMembers || canDeleteMembers))
|
|
170
|
+
(canActivateMembers || canDeleteMembers)))
|
|
151
171
|
"
|
|
152
172
|
v-model="item.menuOpen"
|
|
153
173
|
:close-on-content-click="false"
|
|
@@ -216,6 +236,7 @@
|
|
|
216
236
|
</v-list-item>
|
|
217
237
|
</v-list>
|
|
218
238
|
</v-menu>
|
|
239
|
+
</div>
|
|
219
240
|
</template>
|
|
220
241
|
</v-data-table>
|
|
221
242
|
</div>
|
|
@@ -349,6 +370,8 @@ import {
|
|
|
349
370
|
otherSiteBannerText,
|
|
350
371
|
roleUsableHere,
|
|
351
372
|
safeReturnTo,
|
|
373
|
+
siteAccessLabel,
|
|
374
|
+
limitedSites,
|
|
352
375
|
withCurrentRole,
|
|
353
376
|
} from "../utils/role";
|
|
354
377
|
import { formatLastActive } from "../utils/last-active";
|
|
@@ -457,6 +480,11 @@ const props = defineProps({
|
|
|
457
480
|
},
|
|
458
481
|
});
|
|
459
482
|
|
|
483
|
+
/* The Organization app's Members list: no site, organisation memberships. It
|
|
484
|
+
lists every person (site-limited ones as one row with `limitedTo`), labels
|
|
485
|
+
where each may go, and has no "role from another site" banner - there is no
|
|
486
|
+
"this site" here. */
|
|
487
|
+
const orgLevel = computed(() => !props.siteId && props.type === "organization");
|
|
460
488
|
const items = ref<Array<Record<string, any>>>([]);
|
|
461
489
|
const onlyOtherSite = ref(false);
|
|
462
490
|
const page = ref(1);
|
|
@@ -533,6 +561,8 @@ const {
|
|
|
533
561
|
type: props.type,
|
|
534
562
|
siteId: props.siteId,
|
|
535
563
|
roleState: onlyOtherSite.value ? "other-site" : "",
|
|
564
|
+
// Organization app: site-limited people too, one row each (`limitedTo`).
|
|
565
|
+
people: orgLevel.value,
|
|
536
566
|
}),
|
|
537
567
|
{
|
|
538
568
|
watch: [() => props.status, () => props.orgId, () => props.siteId, headerSearch, onlyOtherSite],
|
|
@@ -548,7 +578,7 @@ const {
|
|
|
548
578
|
const { data: otherSiteReq, refresh: refreshOtherSite } = useAsyncData(
|
|
549
579
|
`members-other-site-${props.type}-${props.orgId}-${props.siteId}-${props.status}`,
|
|
550
580
|
() =>
|
|
551
|
-
props.assignRole && props.orgId
|
|
581
|
+
props.assignRole && props.orgId && !orgLevel.value
|
|
552
582
|
? _getAll({
|
|
553
583
|
status: props.status,
|
|
554
584
|
org: props.orgId,
|
|
@@ -632,6 +662,12 @@ function setMember({
|
|
|
632
662
|
screen, whoever may assign roles can choose a member's sites. The server
|
|
633
663
|
decides again (owner / org-wide admin only). */
|
|
634
664
|
const siteAccessAllowed = computed(() => props.assignRole && !props.siteId);
|
|
665
|
+
/* A site-limited person's row stands for several per-site memberships, so the
|
|
666
|
+
one-membership actions (Assign Role, Suspend, Delete) are not offered on it;
|
|
667
|
+
Site access manages them. */
|
|
668
|
+
const isLimited = (item: Record<string, any>) => orgLevel.value && limitedSites(item).length > 0;
|
|
669
|
+
/* Keep the actions column on screen however wide the table gets. */
|
|
670
|
+
const actionLast = computed(() => props.headers[props.headers.length - 1]?.value === "action-table");
|
|
635
671
|
const siteAccessDialog = ref(false);
|
|
636
672
|
const siteAccessUser = ref("");
|
|
637
673
|
const siteAccessName = ref("");
|
|
@@ -777,20 +813,16 @@ function formatDateInvited(value?: string | null) {
|
|
|
777
813
|
if (Number.isNaN(date.getTime())) return "";
|
|
778
814
|
|
|
779
815
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
780
|
-
month: "
|
|
816
|
+
month: "short",
|
|
781
817
|
day: "numeric",
|
|
782
818
|
year: "numeric",
|
|
783
|
-
hour: "2-digit",
|
|
784
|
-
minute: "2-digit",
|
|
785
|
-
hour12: true,
|
|
786
819
|
}).formatToParts(date);
|
|
787
820
|
|
|
788
821
|
const getPart = (type: Intl.DateTimeFormatPartTypes) =>
|
|
789
822
|
parts.find((part) => part.type === type)?.value ?? "";
|
|
790
823
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
)}:${getPart("minute")}${getPart("dayPeriod").toUpperCase()}`;
|
|
824
|
+
// Short ("Sep 29, 2026") so the table fits and the actions stay in view.
|
|
825
|
+
return `${getPart("month")} ${getPart("day")}, ${getPart("year")}`;
|
|
794
826
|
}
|
|
795
827
|
|
|
796
828
|
async function handleUpdateMemberStatus() {
|
|
@@ -826,3 +858,23 @@ const updateActionText = computed(() => {
|
|
|
826
858
|
return map[updateAction.value] || updateAction.value;
|
|
827
859
|
});
|
|
828
860
|
</script>
|
|
861
|
+
|
|
862
|
+
<style scoped>
|
|
863
|
+
/* The actions column stays pinned to the right edge (owner report 2026-09-29:
|
|
864
|
+
at 1151px the menu sat off-screen behind a sideways scroll). */
|
|
865
|
+
.member-table--pin-action :deep(th:last-child),
|
|
866
|
+
.member-table--pin-action :deep(td:last-child) {
|
|
867
|
+
position: sticky;
|
|
868
|
+
right: 0;
|
|
869
|
+
z-index: 1;
|
|
870
|
+
background: rgb(var(--v-theme-surface));
|
|
871
|
+
box-shadow: -1px 0 0 rgba(var(--v-border-color), var(--v-border-opacity));
|
|
872
|
+
}
|
|
873
|
+
.member-table--pin-action :deep(thead th:last-child) {
|
|
874
|
+
z-index: 3;
|
|
875
|
+
}
|
|
876
|
+
.member-table :deep(.member-role-chip) {
|
|
877
|
+
white-space: normal;
|
|
878
|
+
height: auto;
|
|
879
|
+
}
|
|
880
|
+
</style>
|
package/composables/useMember.ts
CHANGED
|
@@ -70,9 +70,13 @@ export default function useMember() {
|
|
|
70
70
|
status = "active",
|
|
71
71
|
siteId = "",
|
|
72
72
|
roleState = "",
|
|
73
|
+
people = false,
|
|
73
74
|
} = {}) {
|
|
74
75
|
const query: Record<string, any> = { page, limit, type, status };
|
|
75
76
|
|
|
77
|
+
// Organization app Members: every person, site-limited ones as one row
|
|
78
|
+
// with `limitedTo` (core 2026-09-29). Only sent when asked for.
|
|
79
|
+
if (people) query.people = 1;
|
|
76
80
|
if (search) query.search = search;
|
|
77
81
|
// "other-site": only members holding a role pinned to another site.
|
|
78
82
|
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.
|
|
5
|
+
"version": "4.75.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.",
|
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" };
|
|
@@ -95,3 +111,19 @@ export function safeReturnTo(value: unknown): string {
|
|
|
95
111
|
const path = Array.isArray(value) ? value[0] : value;
|
|
96
112
|
return typeof path === "string" && /^\/(?!\/)[^\s\\]*$/.test(path) ? path : "";
|
|
97
113
|
}
|
|
114
|
+
|
|
115
|
+
type TLimitedSite = { siteId?: string; siteName?: string; roleName?: string };
|
|
116
|
+
|
|
117
|
+
/** The sites a site-limited person may open (server `limitedTo`; org Members). */
|
|
118
|
+
export function limitedSites(item: { limitedTo?: unknown } | null | undefined): TLimitedSite[] {
|
|
119
|
+
return Array.isArray(item?.limitedTo) ? (item!.limitedTo as TLimitedSite[]) : [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The Organization app's Members "Sites" cell (owner report 2026-09-29, Aigle
|
|
124
|
+
* / Kanesan): a person limited to sites stays on the list and says where.
|
|
125
|
+
*/
|
|
126
|
+
export function siteAccessLabel(item: { limitedTo?: unknown } | null | undefined): string {
|
|
127
|
+
const names = limitedSites(item).map((l) => l.siteName || "a site");
|
|
128
|
+
return names.length ? `Limited to: ${names.join(", ")}` : "All sites";
|
|
129
|
+
}
|