@7365admin1/layer-common 4.74.0 → 4.75.1
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 +16 -4
- package/components/MemberMain.vue +68 -13
- package/composables/useMember.ts +4 -0
- package/package.json +1 -1
- package/utils/role.ts +43 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.75.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b84da6e: Organization Members: keep the "Fix roles from other sites" banner at organisation level, worded for the organisation ("N members hold a role created for one particular site…"), without the per-site "Show them" filter. 4.75.0 had hidden the whole banner there.
|
|
8
|
+
|
|
9
|
+
## 4.75.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 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).
|
|
14
|
+
|
|
3
15
|
## 4.74.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -1896,11 +1908,11 @@
|
|
|
1896
1908
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1897
1909
|
own rule from two endpoints the console already calls, unprojected:
|
|
1898
1910
|
|
|
1899
|
-
|
|
1900
|
-
|
|
1911
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1912
|
+
GET /api/roles/id/:role that membership's role document
|
|
1901
1913
|
|
|
1902
|
-
|
|
1903
|
-
|
|
1914
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1915
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1904
1916
|
|
|
1905
1917
|
`role.default` is the marker because it is the only property of a platform
|
|
1906
1918
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -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
|
@@ -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.1",
|
|
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
|
+
}
|