@7365admin1/layer-common 4.50.0 → 4.51.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.51.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 39cbe08: Fix the robot pages crashing in the browser on 4.50.0/4.51.0 ("process is not defined" / "Couldn't resolve component default"). A parameter named `measure` in `utils/rmf-editor.ts` was picked up by Nuxt auto-imports as the `measure` export of `utils/theme-aa-ledger.ts`, whose top-level command-line check read `process.argv`. The parameter is renamed and the ledger's check is guarded; a new test evaluates every util in a browser-like scope with the auto-imports injected.
8
+
9
+ ## 4.51.0
10
+
11
+ ### Minor Changes
12
+
13
+ - ac247e2: Members: a banner "N members here have a role from another site. Assign each one a role for this site." with a one-click filter to list them (server `roleState=other-site`); disappears at 0. `useMember().getAll` accepts `roleState`.
14
+
3
15
  ## 4.50.0
4
16
 
5
17
  ### Minor Changes
@@ -1539,11 +1551,11 @@
1539
1551
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
1540
1552
  own rule from two endpoints the console already calls, unprojected:
1541
1553
 
1542
- GET /api/members/user/:user/app/admin the Seven365 staff membership
1543
- GET /api/roles/id/:role that membership's role document
1554
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
1555
+ GET /api/roles/id/:role that membership's role document
1544
1556
 
1545
- owner = member.type === "admin" && role.type === "admin" && role.default === true
1546
- staff = member.type === "admin" && role.type === "admin"
1557
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
1558
+ staff = member.type === "admin" && role.type === "admin"
1547
1559
 
1548
1560
  `role.default` is the marker because it is the only property of a platform
1549
1561
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -27,6 +27,28 @@
27
27
  </template>
28
28
  </PageHeader>
29
29
 
30
+ <!-- Owner decision 2026-09-25: members holding ANOTHER site's role keep
31
+ their access; the company assigns each one a role for this site.
32
+ Shown while any remain (server count), gone at 0. -->
33
+ <v-alert
34
+ v-if="props.assignRole && otherSiteBanner"
35
+ type="warning"
36
+ variant="tonal"
37
+ density="compact"
38
+ class="mb-3"
39
+ data-test="member-other-site-banner"
40
+ >
41
+ <div class="d-flex align-center flex-wrap ga-2">
42
+ <span class="flex-grow-1">{{ otherSiteBanner }}</span>
43
+ <AppButton
44
+ data-test="member-other-site-filter"
45
+ @click="toggleOtherSite()"
46
+ >
47
+ {{ onlyOtherSite ? "Show all members" : "Show them" }}
48
+ </AppButton>
49
+ </div>
50
+ </v-alert>
51
+
30
52
  <AppCard class="table-card">
31
53
  <CardToolbar :count="pageRange">
32
54
  <template #tabs>
@@ -278,7 +300,7 @@
278
300
  </template>
279
301
 
280
302
  <script setup lang="ts">
281
- import { memberRoleChip, withCurrentRole } from "../utils/role";
303
+ import { memberRoleChip, otherSiteBannerText, withCurrentRole } from "../utils/role";
282
304
 
283
305
  const props = defineProps({
284
306
  /** The design's page title. Every app that mounts this screen calls it
@@ -372,6 +394,7 @@ const props = defineProps({
372
394
  });
373
395
 
374
396
  const items = ref<Array<Record<string, any>>>([]);
397
+ const onlyOtherSite = ref(false);
375
398
  const page = ref(1);
376
399
  const pages = ref(10);
377
400
  const pageRange = ref("-- - -- of --");
@@ -444,12 +467,47 @@ const {
444
467
  page: page.value,
445
468
  type: props.type,
446
469
  siteId: props.siteId,
470
+ roleState: onlyOtherSite.value ? "other-site" : "",
447
471
  }),
448
472
  {
449
- watch: [() => props.status, () => props.orgId, () => props.siteId, headerSearch],
473
+ watch: [() => props.status, () => props.orgId, () => props.siteId, headerSearch, onlyOtherSite],
450
474
  }
451
475
  );
452
476
 
477
+ /*
478
+ * "N members here have a role from another site" — the count is the server's
479
+ * (`roleState=other-site`, `total`), for this org / site / app, so it is right
480
+ * however many pages the list has. Refreshed with the list (after an Assign
481
+ * Role the list refreshes, and so does this).
482
+ */
483
+ const { data: otherSiteReq, refresh: refreshOtherSite } = useAsyncData(
484
+ `members-other-site-${props.type}-${props.orgId}-${props.siteId}-${props.status}`,
485
+ () =>
486
+ props.assignRole && props.orgId
487
+ ? _getAll({
488
+ status: props.status,
489
+ org: props.orgId,
490
+ page: 1,
491
+ limit: 1,
492
+ type: props.type,
493
+ siteId: props.siteId,
494
+ roleState: "other-site",
495
+ }).catch(() => null)
496
+ : Promise.resolve(null),
497
+ { watch: [() => props.status, () => props.orgId, () => props.siteId] }
498
+ );
499
+ const otherSiteBanner = computed(() =>
500
+ otherSiteBannerText(otherSiteReq.value?.total)
501
+ );
502
+ watch(otherSiteBanner, (text) => {
503
+ // the last one fixed: back to the full list
504
+ if (!text && onlyOtherSite.value) onlyOtherSite.value = false;
505
+ });
506
+ function toggleOtherSite() {
507
+ page.value = 1;
508
+ onlyOtherSite.value = !onlyOtherSite.value;
509
+ }
510
+
453
511
  const loading = computed(() => getAllReqStatus.value === "pending");
454
512
 
455
513
  watch(headerSearch, () => {
@@ -567,7 +625,7 @@ async function updateMemberRole() {
567
625
  props.orgId
568
626
  );
569
627
  await setMember({ mode: "assign-role" });
570
- await getAll();
628
+ await Promise.all([getAll(), refreshOtherSite()]);
571
629
  } catch (error: any) {
572
630
  // `message.value` alone sets the snackbar's TEXT and never opens it, so a
573
631
  // refused role change said nothing at all - the dialog just closed and the
@@ -69,10 +69,13 @@ export default function useMember() {
69
69
  type = "",
70
70
  status = "active",
71
71
  siteId = "",
72
+ roleState = "",
72
73
  } = {}) {
73
74
  const query: Record<string, any> = { page, limit, type, status };
74
75
 
75
76
  if (search) query.search = search;
77
+ // "other-site": only members holding a role pinned to another site.
78
+ if (roleState) query.roleState = roleState;
76
79
  if (user) query.user = user;
77
80
  if (org) query.org = org;
78
81
  if (siteId) query.siteId = siteId;
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.50.0",
5
+ "version": "4.51.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.",
@@ -249,9 +249,9 @@ export function alignFloor(shapes: TEditorShape[], f: TFrame, floorNo: number, r
249
249
  * about the plan's top-left corner - so the plan and the drawing stay
250
250
  * together. Null factor with a reason when it cannot be done.
251
251
  */
252
- export function measureScale(f: TFrame, measure: TEditorShape, realM: number, placement: TPlanPlacement | null) {
252
+ export function measureScale(f: TFrame, line: TEditorShape, realM: number, placement: TPlanPlacement | null) {
253
253
  if (!(realM > 0) || !Number.isFinite(realM)) return { error: "Enter the real length in metres (more than 0)." } as const;
254
- const [a, b] = measure.points.map((p) => shareToWorld(f, p));
254
+ const [a, b] = line.points.map((p) => shareToWorld(f, p));
255
255
  const drawn = Math.hypot(b!.x - a!.x, b!.y - a!.y);
256
256
  if (drawn < 0.05) return { error: "The two ends are too close together - measure a longer length." } as const;
257
257
  const k = realM / drawn;
package/utils/role.ts CHANGED
@@ -60,3 +60,16 @@ export function memberRoleChip(item: {
60
60
  return name ? { text: name, tone: "info" } : null;
61
61
  }
62
62
  }
63
+
64
+ /**
65
+ * The Members banner (owner decision 2026-09-25: keep the warning, companies
66
+ * re-assign a role for the right site, nobody's access is cut). `null` at 0 so
67
+ * the banner disappears once every member here has this site's role.
68
+ */
69
+ export function otherSiteBannerText(count: unknown): string | null {
70
+ const n = typeof count === "number" && Number.isFinite(count) ? Math.floor(count) : 0;
71
+ if (n <= 0) return null;
72
+ return n === 1
73
+ ? "1 member here has a role from another site. Assign them a role for this site."
74
+ : `${n} members here have a role from another site. Assign each one a role for this site.`;
75
+ }
@@ -134,7 +134,8 @@ export const DARK_PAIRS = () => {
134
134
  ].map((p) => ({ ...p, ratio: Math.round(contrast(p.fg, p.bg) * 100) / 100, passes: contrast(p.fg, p.bg) >= p.need }));
135
135
  };
136
136
 
137
- if (process.argv[1]?.endsWith("theme-aa-ledger.ts")) {
137
+ // Guarded: this file is auto-importable, and a browser has no `process` (an unguarded check crashed the robot pages in 4.50.0).
138
+ if (typeof process !== "undefined" && process.argv?.[1]?.endsWith("theme-aa-ledger.ts")) {
138
139
  const row = (r: { what: string; fg: string; bg: string; ratio: number; need: number; passes: boolean }) =>
139
140
  `${r.passes ? "PASS" : "FAIL"} ${String(r.ratio).padStart(5)}:1 / ${r.need} ${r.fg} on ${r.bg} ${r.what}`;
140
141
  console.log("LIGHT");