@7365admin1/layer-common 3.2.8-staging.215 → 3.2.8-staging.218

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.
@@ -1343,27 +1343,41 @@ const { userAppRole } = useLocalSetup();
1343
1343
  const { downloadPDF } = usePDFDownload();
1344
1344
  const route = useRoute();
1345
1345
 
1346
+ // Each app spells its own resources, and the three that render this dashboard
1347
+ // disagree: work orders are `work_orders` in `-security`, `workOrder` in
1348
+ // `-property-management` and `work_order` in this layer's `useCleaningPermission`;
1349
+ // visitors are `visitor-mgmt` in `-security` and `visitorManagement` in
1350
+ // `-property-management`. A widget asking for one spelling is invisible to a role
1351
+ // granted another, so every spelling in use is listed. One canonical vocabulary
1352
+ // would let all of these collapse back to a single string -- that is a
1353
+ // cross-repo change, not one this component can make.
1354
+ const WORK_ORDER_SEE_ALL = [
1355
+ "work_orders:see-all-work-orders",
1356
+ "workOrder:see-all-work-orders",
1357
+ "work_order:see-all-work-orders",
1358
+ ];
1359
+
1346
1360
  const propertyWidgetPermissions: Record<string, string[]> = {
1347
- workOrders: ["work_orders:see-all-work-orders"],
1361
+ workOrders: WORK_ORDER_SEE_ALL,
1348
1362
  incidents: ["incident-reports:see-incident-reports"],
1349
- visitors: ["visitor-mgmt:see-all-visitor"],
1363
+ visitors: ["visitor-mgmt:see-all-visitor", "visitorManagement:see-all-visitor"],
1350
1364
  facilityBookings: ["facility-booking-mgmt:see-all-facility-booking"],
1351
- activity: ["work_orders:see-all-work-orders"],
1365
+ activity: WORK_ORDER_SEE_ALL,
1352
1366
  upcomingEvents: ["event-mgmt:see-all-event"],
1353
1367
  todayReminders: ["event-mgmt:see-all-event"],
1354
1368
  todayAttentions: [
1355
1369
  "incident-reports:see-incident-reports",
1356
1370
  "facility-booking-mgmt:see-all-facility-booking",
1357
1371
  ],
1358
- workOrderStatus: ["work_orders:see-all-work-orders"],
1372
+ workOrderStatus: WORK_ORDER_SEE_ALL,
1359
1373
  feedbacks: ["feedbacks:see-all-feedback"],
1360
1374
  activePatrol: ["virtual-patrol:see-all-virtual-patrol-logs"],
1361
1375
  };
1362
1376
 
1363
1377
  const moduleWidgetPermissions: Record<string, string[]> = {
1364
- openWorkOrder: ["work_orders:see-all-work-orders"],
1365
- workOrders: ["work_orders:see-all-work-orders"],
1366
- activity: ["work_orders:see-all-work-orders"],
1378
+ openWorkOrder: WORK_ORDER_SEE_ALL,
1379
+ workOrders: WORK_ORDER_SEE_ALL,
1380
+ activity: WORK_ORDER_SEE_ALL,
1367
1381
  taskSchedule: [
1368
1382
  "cleaning-schedule-mgmt:see-all-schedules",
1369
1383
  "schedule-task-mgmt:see-all-schedule-tasks",
@@ -1406,7 +1420,12 @@ function canViewWidget(key: string) {
1406
1420
  ? propertyWidgetPermissions[key]
1407
1421
  : moduleWidgetPermissions[key];
1408
1422
 
1409
- if (!permissions) return true;
1423
+ // Fail closed. This used to return `true` while `userAppRole` was still
1424
+ // unresolved, so the dashboard drew every widget and then took some away --
1425
+ // and if the role fetch failed outright it drew every widget permanently.
1426
+ // Every other gate in this package returns `false` without a role; this one
1427
+ // now agrees with them.
1428
+ if (!permissions) return false;
1410
1429
  if (permissions.includes("*")) return true;
1411
1430
 
1412
1431
  if (
@@ -84,8 +84,16 @@
84
84
  {{ formatDateInvited(item.dateInvited) }}
85
85
  </template>
86
86
  <template #item.action-table="{ item }">
87
+ <!--
88
+ `props.assignRole` belongs in this test. The menu's only item that
89
+ is not status-dependent is Assign Role, but the menu itself opened
90
+ only for suspend / activate / delete -- so a role granted
91
+ `members:assign-member-role` and nothing else got no menu at all,
92
+ and the permission was unreachable.
93
+ -->
87
94
  <v-menu
88
95
  v-if="
96
+ props.assignRole ||
89
97
  (currentStatus === 'active' &&
90
98
  (canSuspendMembers || canDeleteMembers)) ||
91
99
  (currentStatus === 'suspended' &&
@@ -317,7 +317,12 @@ async function submit() {
317
317
  });
318
318
  emit("success");
319
319
  } catch (error: any) {
320
- message.value = error.response._data.message;
320
+ // Same unguarded chain as the preview dialog had: on a network or timeout
321
+ // failure `error.response` is undefined and the catch threw inside itself,
322
+ // leaving the user with no message at all.
323
+ message.value =
324
+ error?.response?._data?.message ??
325
+ "Could not create this role. Please check your connection and try again.";
321
326
  } finally {
322
327
  disable.value = false;
323
328
  }
@@ -99,15 +99,12 @@
99
99
  </div>
100
100
  </v-col>
101
101
 
102
- <v-col v-if="edit" cols="12" class="my-2">
103
- <v-row no-gutters>
104
- <v-col cols="12" class="text-center">
105
- <span class="role-perm__error">{{ message }}</span>
106
- </v-col>
107
- </v-row>
108
- </v-col>
109
-
110
- <v-col cols="12" class="my-2">
102
+ <!--
103
+ One message, once. This block used to appear twice -- inside
104
+ `v-if="edit"` and again unconditionally -- so a refused save printed
105
+ the server's sentence back to back.
106
+ -->
107
+ <v-col v-if="message" cols="12" class="my-2">
111
108
  <v-row no-gutters>
112
109
  <v-col cols="12" class="text-center">
113
110
  <span class="role-perm__error">{{ message }}</span>
@@ -122,7 +119,7 @@
122
119
  <AppButton v-if="!edit" variant="ghost" @click="emit('cancel')">Close</AppButton>
123
120
  <AppButton v-else variant="ghost" @click="edit = false">Cancel</AppButton>
124
121
 
125
- <AppButton v-if="!edit" :disabled="disable" @click="edit = true">Edit</AppButton>
122
+ <AppButton v-if="!edit && canUpdateRole" :disabled="disable" @click="edit = true">Edit</AppButton>
126
123
  <AppButton v-else :disabled="Boolean(errorMessages) || disable" @click="submit()">
127
124
  Submit
128
125
  </AppButton>
@@ -159,6 +156,15 @@ const props = defineProps({
159
156
  type: Array as PropType<string[]>,
160
157
  default: () => [],
161
158
  },
159
+ // "Update role". This dialog had no permission prop at all, so it showed Edit
160
+ // to everyone -- including on the seeded platform-owner role -- and the
161
+ // `canUpdateRole` the apps have been passing to `RolePermissionMain` in good
162
+ // faith did nothing. `RolePermissionMain` is the only thing that renders this
163
+ // dialog and always passes it, so the default is closed.
164
+ canUpdateRole: {
165
+ type: Boolean,
166
+ default: false,
167
+ },
162
168
  });
163
169
 
164
170
  const validForm = ref(false);
@@ -282,7 +288,12 @@ async function submit() {
282
288
  await updatePermissionById(props.id, definedModel.value);
283
289
  emit("success");
284
290
  } catch (error: any) {
285
- message.value = error.response._data.message;
291
+ // `error.response` is undefined on a network or timeout failure, so the old
292
+ // unguarded chain threw inside its own catch: nothing was shown, the button
293
+ // came back enabled, and the save looked like it had simply been ignored.
294
+ message.value =
295
+ error?.response?._data?.message ??
296
+ "Could not save this role. Please check your connection and try again.";
286
297
  } finally {
287
298
  disable.value = false;
288
299
  }
@@ -42,12 +42,13 @@
42
42
 
43
43
  <v-data-table
44
44
  :headers="props.headers"
45
- :items="items"
45
+ :items="visibleItems"
46
46
  item-value="_id"
47
47
  items-per-page="20"
48
48
  fixed-header
49
49
  hide-default-footer
50
50
  hide-default-header
51
+ :no-data-text="emptyText"
51
52
  @click:row="tableRowClickHandler"
52
53
  style="max-height: calc(100vh - (180px))"
53
54
  >
@@ -102,6 +103,7 @@
102
103
  v-model:edit="edit"
103
104
  :name="name"
104
105
  :id="roleId"
106
+ :can-update-role="props.canUpdateRole"
105
107
  :web-only-resources="props.webOnlyResources"
106
108
  />
107
109
  </v-dialog>
@@ -245,13 +247,18 @@ const props = defineProps({
245
247
  type: Boolean,
246
248
  default: false,
247
249
  },
250
+ // "See all roles". Defaults to `true` because nine of the ten pages that
251
+ // render this component do not pass it, and until now nothing read it at all
252
+ // -- so `true` is exactly what those pages have always had. The one page that
253
+ // does pass it now gets a gate that works.
248
254
  canViewRole: {
249
255
  type: Boolean,
250
- default: false,
256
+ default: true,
251
257
  },
258
+ // "See role details" -- opening a role. Same reasoning as `canViewRole`.
252
259
  canViewByRole: {
253
260
  type: Boolean,
254
- default: false,
261
+ default: true,
255
262
  },
256
263
  canDeleteRole: {
257
264
  type: Boolean,
@@ -281,6 +288,7 @@ const {
281
288
  data: getRoleReq,
282
289
  refresh: getRoles,
283
290
  status: getRoleReqStatus,
291
+ error: getRoleReqError,
284
292
  } = useLazyAsyncData(
285
293
  "roles-permissions-get-all",
286
294
  () =>
@@ -305,7 +313,33 @@ watchEffect(() => {
305
313
  }
306
314
  });
307
315
 
316
+ // "See all roles". Without it the list is not drawn at all, and `emptyText`
317
+ // below says so rather than leaving a blank table to be read as a failure.
318
+ const visibleItems = computed(() => (props.canViewRole ? items.value : []));
319
+
320
+ /**
321
+ * SAY WHY THE TABLE IS EMPTY. "No data available" was shown for three
322
+ * different situations -- you may not see roles, the request failed, and this
323
+ * organisation genuinely has none -- so a refusal was indistinguishable from an
324
+ * empty list and neither was distinguishable from a broken screen.
325
+ */
326
+ const emptyText = computed(() => {
327
+ if (!props.canViewRole) return "You do not have permission to view roles.";
328
+ const status = (getRoleReqError.value as any)?.response?.status
329
+ ?? (getRoleReqError.value as any)?.statusCode;
330
+ if (status === 401 || status === 403) {
331
+ return "You do not have permission to view the roles for this organisation.";
332
+ }
333
+ if (getRoleReqError.value) {
334
+ return "Could not load roles. Please refresh to try again.";
335
+ }
336
+ return "No roles have been created yet.";
337
+ });
338
+
308
339
  function tableRowClickHandler(_: any, data: any) {
340
+ // "See role details". Ungated until now, which is why granting or withholding
341
+ // it changed nothing.
342
+ if (!props.canViewByRole) return;
309
343
  previewDialog.value = true;
310
344
  roleId.value = data.item._id;
311
345
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.2.8-staging.215",
5
+ "version": "3.2.8-staging.218",
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.",
@@ -35,7 +35,7 @@
35
35
  "build": "nuxt build .playground",
36
36
  "generate": "nuxt generate .playground",
37
37
  "preview": "nuxt preview .playground",
38
- "test": "esbuild composables/useVisitorSocket.ts --format=esm --outfile=test/.build/useVisitorSocket.mjs --log-level=error && node --test \"test/*.test.mjs\" && yarn test:units",
38
+ "test": "esbuild composables/useVisitorSocket.ts --format=esm --outfile=test/.build/useVisitorSocket.mjs --log-level=error && node --experimental-strip-types --test \"test/*.test.mjs\" && yarn test:units",
39
39
  "test:units": "node --experimental-strip-types --test \"utils/*.test.ts\"",
40
40
  "release": "yarn run build && changeset publish"
41
41
  },