@7365admin1/layer-common 3.2.2-staging.82 → 3.2.2-staging.84
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/FormDialog.vue +60 -21
- package/components/RolePermissionMain.vue +136 -43
- package/components/StatusChip.vue +64 -0
- package/components/TableMain.vue +175 -17
- package/composables/useRole.ts +22 -0
- package/package.json +1 -1
|
@@ -1,31 +1,35 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
PHASE 3 - THE MODAL.
|
|
3
|
+
|
|
4
|
+
520px, 16px radius and the design's overlay and shadow, with the BODY as the
|
|
5
|
+
only thing that scrolls so the footer never leaves the screen. Same dialog,
|
|
6
|
+
same slots, same close behaviour, same `persistent`.
|
|
7
|
+
|
|
8
|
+
The two `theme.name.value === ...` branches are gone, and that is a fix, not
|
|
9
|
+
a preference: they hard-coded `bg-white` and Vuetify's `grey-darken-4`, so
|
|
10
|
+
the card was painted by a literal colour instead of by the theme it is in.
|
|
11
|
+
`--card` is that surface in each theme.
|
|
12
|
+
-->
|
|
1
13
|
<template>
|
|
2
|
-
<v-dialog v-model="model" max-width="
|
|
3
|
-
<v-card
|
|
4
|
-
class="px-3 d-flex flex-column"
|
|
5
|
-
:class="`${theme.name.value === 'light' ? 'bg-white' : ''} ${
|
|
6
|
-
mdAndUp ? 'rounded-xl' : ''
|
|
7
|
-
}`"
|
|
8
|
-
:style="{ border: 'none', boxShadow: 'none' }"
|
|
9
|
-
>
|
|
14
|
+
<v-dialog v-model="model" :max-width="520" persistent class="form-dialog">
|
|
15
|
+
<v-card class="d-flex flex-column form-dialog__card" :style="{ border: 'none' }">
|
|
10
16
|
<v-toolbar
|
|
11
|
-
|
|
12
|
-
class="flex-shrink-0
|
|
17
|
+
color="transparent"
|
|
18
|
+
class="flex-shrink-0 form-dialog__head"
|
|
13
19
|
style="position: sticky; top: 0; z-index: 10"
|
|
14
20
|
>
|
|
15
21
|
<slot name="title">
|
|
16
|
-
<span class="
|
|
17
|
-
Dialog
|
|
18
|
-
</span>
|
|
22
|
+
<span class="form-dialog__title"> Dialog </span>
|
|
19
23
|
</slot>
|
|
20
24
|
<v-spacer />
|
|
21
|
-
<v-btn icon="mdi-close" @click="closeDialog" />
|
|
25
|
+
<v-btn icon="mdi-close" variant="text" @click="closeDialog" />
|
|
22
26
|
</v-toolbar>
|
|
23
27
|
|
|
24
|
-
<v-sheet class="flex-grow-1 overflow-y-auto px-
|
|
28
|
+
<v-sheet class="flex-grow-1 overflow-y-auto px-4 form-dialog__body">
|
|
25
29
|
<slot />
|
|
26
30
|
</v-sheet>
|
|
27
31
|
|
|
28
|
-
<v-sheet class="flex-shrink-0 px-
|
|
32
|
+
<v-sheet class="flex-shrink-0 px-4 py-3 form-dialog__footer">
|
|
29
33
|
<slot name="footer" />
|
|
30
34
|
</v-sheet>
|
|
31
35
|
</v-card>
|
|
@@ -33,11 +37,6 @@
|
|
|
33
37
|
</template>
|
|
34
38
|
|
|
35
39
|
<script setup lang="ts">
|
|
36
|
-
import { useTheme, useDisplay } from "vuetify";
|
|
37
|
-
|
|
38
|
-
const theme = useTheme().global;
|
|
39
|
-
const { mdAndUp } = useDisplay();
|
|
40
|
-
|
|
41
40
|
const props = defineProps<{
|
|
42
41
|
modelValue: boolean;
|
|
43
42
|
}>();
|
|
@@ -62,4 +61,44 @@ function closeDialog() {
|
|
|
62
61
|
:deep(.v-dialog .v-overlay__content) {
|
|
63
62
|
margin: auto;
|
|
64
63
|
}
|
|
64
|
+
|
|
65
|
+
.form-dialog__card {
|
|
66
|
+
font-family: var(--font-sans);
|
|
67
|
+
background: var(--card);
|
|
68
|
+
/* `!important` because Vuetify's own `.v-overlay__content > .v-card` rule is
|
|
69
|
+
more specific than a class on the card, and it is what keeps the dialog at
|
|
70
|
+
its factory 4px. */
|
|
71
|
+
border-radius: var(--r-modal) !important;
|
|
72
|
+
box-shadow: var(--shadow-modal);
|
|
73
|
+
/* Never taller than the screen it has to fit inside; the body is what
|
|
74
|
+
scrolls, so the Cancel/Submit footer cannot be pushed out of view. */
|
|
75
|
+
max-height: calc(100vh - 32px);
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.form-dialog__head {
|
|
80
|
+
border-bottom: 1px solid var(--border);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.form-dialog__title {
|
|
84
|
+
font-size: var(--fs-card-title);
|
|
85
|
+
font-weight: var(--fw-card-title);
|
|
86
|
+
color: var(--text);
|
|
87
|
+
text-transform: capitalize;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.form-dialog__body,
|
|
91
|
+
.form-dialog__footer {
|
|
92
|
+
background: var(--card);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.form-dialog__footer {
|
|
96
|
+
border-top: 1px solid var(--border);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@media (max-width: 599.98px) {
|
|
100
|
+
.form-dialog__card {
|
|
101
|
+
max-height: calc(100dvh - 32px);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
65
104
|
</style>
|
|
@@ -105,42 +105,90 @@
|
|
|
105
105
|
/>
|
|
106
106
|
</v-dialog>
|
|
107
107
|
|
|
108
|
-
<
|
|
109
|
-
v-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
</
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
108
|
+
<v-dialog v-model="confirmDialog" max-width="450">
|
|
109
|
+
<v-card>
|
|
110
|
+
<v-toolbar>
|
|
111
|
+
<v-row no-gutters class="fill-height px-6" align="center">
|
|
112
|
+
<span class="font-weight-bold text-h6 text-capitalize">
|
|
113
|
+
Delete Role
|
|
114
|
+
</span>
|
|
115
|
+
</v-row>
|
|
116
|
+
</v-toolbar>
|
|
117
|
+
|
|
118
|
+
<v-card-text>
|
|
119
|
+
<span v-if="!hasAssignedMembers" class="text-subtitle-2">
|
|
120
|
+
Are you sure you want to delete this role? This action cannot be
|
|
121
|
+
undone.
|
|
122
|
+
</span>
|
|
123
|
+
<template v-else>
|
|
124
|
+
<p class="text-subtitle-2 mb-4">
|
|
125
|
+
There are currently
|
|
126
|
+
<span class="text-error font-weight-bold">
|
|
127
|
+
{{ deletionPreview.members.length }}
|
|
128
|
+
{{ deletionPreview.members.length === 1 ? "user" : "users" }}
|
|
129
|
+
</span>
|
|
130
|
+
assigned to this role. Reassign each user before deleting it so
|
|
131
|
+
they do not lose access.
|
|
132
|
+
</p>
|
|
133
|
+
|
|
134
|
+
<v-row
|
|
135
|
+
v-for="member in deletionPreview.members"
|
|
136
|
+
:key="member._id"
|
|
137
|
+
no-gutters
|
|
138
|
+
class="mb-3 pa-3 border rounded-lg"
|
|
139
|
+
align="center"
|
|
140
|
+
>
|
|
141
|
+
<v-col cols="12" sm="5" class="text-body-2 mb-2 mb-sm-0">
|
|
142
|
+
{{ member.name || member.user }}
|
|
143
|
+
</v-col>
|
|
144
|
+
<v-col cols="12" sm="7">
|
|
145
|
+
<v-select
|
|
146
|
+
v-model="memberReassignments[member._id]"
|
|
147
|
+
:items="deletionPreview.replacementRoles"
|
|
148
|
+
item-title="name"
|
|
149
|
+
item-value="_id"
|
|
150
|
+
label="Reassign role"
|
|
151
|
+
density="comfortable"
|
|
152
|
+
hide-details
|
|
153
|
+
/>
|
|
154
|
+
</v-col>
|
|
155
|
+
</v-row>
|
|
156
|
+
</template>
|
|
157
|
+
</v-card-text>
|
|
158
|
+
|
|
159
|
+
<v-toolbar class="pa-0" density="compact">
|
|
160
|
+
<v-row no-gutters>
|
|
161
|
+
<v-col cols="6" class="pa-0">
|
|
162
|
+
<v-btn
|
|
163
|
+
block
|
|
164
|
+
variant="text"
|
|
165
|
+
class="text-none"
|
|
166
|
+
size="large"
|
|
167
|
+
height="48"
|
|
168
|
+
@click="closeDeleteDialog"
|
|
169
|
+
:disabled="deleteLoading"
|
|
170
|
+
>
|
|
171
|
+
Cancel
|
|
172
|
+
</v-btn>
|
|
173
|
+
</v-col>
|
|
174
|
+
<v-col cols="6" class="pa-0">
|
|
175
|
+
<v-btn
|
|
176
|
+
block
|
|
177
|
+
variant="flat"
|
|
178
|
+
color="black"
|
|
179
|
+
class="text-none font-weight-bold rounded-0"
|
|
180
|
+
height="48"
|
|
181
|
+
@click="handleDeleteRole"
|
|
182
|
+
:loading="deleteLoading"
|
|
183
|
+
:disabled="hasAssignedMembers && !canConfirmDeletion"
|
|
184
|
+
>
|
|
185
|
+
{{ hasAssignedMembers ? "Confirm" : "Delete" }}
|
|
186
|
+
</v-btn>
|
|
187
|
+
</v-col>
|
|
188
|
+
</v-row>
|
|
189
|
+
</v-toolbar>
|
|
190
|
+
</v-card>
|
|
191
|
+
</v-dialog>
|
|
144
192
|
|
|
145
193
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
146
194
|
</v-row>
|
|
@@ -367,10 +415,49 @@ async function refreshRolesAfterUpdate(savedPermissions: string[]) {
|
|
|
367
415
|
const confirmDialog = ref(false);
|
|
368
416
|
const selectedRoleId = ref<string | null>(null);
|
|
369
417
|
const deleteLoading = ref(false);
|
|
418
|
+
const deletionPreview = ref({
|
|
419
|
+
members: [] as Array<Record<string, any>>,
|
|
420
|
+
replacementRoles: [] as Array<Record<string, any>>,
|
|
421
|
+
});
|
|
422
|
+
const memberReassignments = ref<Record<string, string>>({});
|
|
423
|
+
|
|
424
|
+
const hasAssignedMembers = computed(
|
|
425
|
+
() => deletionPreview.value.members.length > 0
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const canConfirmDeletion = computed(() =>
|
|
429
|
+
deletionPreview.value.members.every(
|
|
430
|
+
(member) => Boolean(memberReassignments.value[member._id])
|
|
431
|
+
)
|
|
432
|
+
);
|
|
370
433
|
|
|
371
|
-
|
|
434
|
+
const { getDeletionPreview, deleteWithReassignments } = useRole();
|
|
435
|
+
|
|
436
|
+
async function openDeleteDialog(id: string) {
|
|
372
437
|
selectedRoleId.value = id;
|
|
373
|
-
|
|
438
|
+
deletionPreview.value = { members: [], replacementRoles: [] };
|
|
439
|
+
memberReassignments.value = {};
|
|
440
|
+
deleteLoading.value = true;
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const res = await getDeletionPreview(id);
|
|
444
|
+
deletionPreview.value = res.data;
|
|
445
|
+
confirmDialog.value = true;
|
|
446
|
+
} catch (error: any) {
|
|
447
|
+
const errorMessage =
|
|
448
|
+
error?.response?._data?.message || "Failed to load role assignments.";
|
|
449
|
+
showMessage(errorMessage, "error");
|
|
450
|
+
selectedRoleId.value = null;
|
|
451
|
+
} finally {
|
|
452
|
+
deleteLoading.value = false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function closeDeleteDialog() {
|
|
457
|
+
confirmDialog.value = false;
|
|
458
|
+
selectedRoleId.value = null;
|
|
459
|
+
deletionPreview.value = { members: [], replacementRoles: [] };
|
|
460
|
+
memberReassignments.value = {};
|
|
374
461
|
}
|
|
375
462
|
|
|
376
463
|
function showMessage(msg: string, color: string) {
|
|
@@ -383,17 +470,23 @@ async function handleDeleteRole() {
|
|
|
383
470
|
if (!selectedRoleId.value) return;
|
|
384
471
|
deleteLoading.value = true;
|
|
385
472
|
try {
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
473
|
+
const reassignments = deletionPreview.value.members.map((member) => ({
|
|
474
|
+
memberId: member._id,
|
|
475
|
+
roleId: memberReassignments.value[member._id],
|
|
476
|
+
}));
|
|
477
|
+
const res = hasAssignedMembers.value
|
|
478
|
+
? await deleteWithReassignments(selectedRoleId.value, reassignments)
|
|
479
|
+
: await deleteRole(selectedRoleId.value);
|
|
480
|
+
|
|
481
|
+
closeDeleteDialog();
|
|
389
482
|
showMessage(res.message, "success");
|
|
390
483
|
getRoles();
|
|
391
484
|
} catch (error: any) {
|
|
392
|
-
const errorMessage =
|
|
485
|
+
const errorMessage =
|
|
486
|
+
error?.response?._data?.message || "Failed to delete role.";
|
|
393
487
|
showMessage(errorMessage, "error");
|
|
394
488
|
} finally {
|
|
395
489
|
deleteLoading.value = false;
|
|
396
|
-
selectedRoleId.value = null;
|
|
397
490
|
}
|
|
398
491
|
}
|
|
399
492
|
</script>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE STATUS CHIP.
|
|
3
|
+
|
|
4
|
+
A pill in the tone the design assigns to the status WORD, with the 6px
|
|
5
|
+
leading dot. The word -> tone mapping is `utils/status.ts` and is shared, so
|
|
6
|
+
"Open" is the same green on a work order and on a Daily Occurrence Book, and
|
|
7
|
+
a status the design never named comes out neutral rather than guessed.
|
|
8
|
+
|
|
9
|
+
Colour only. The chip prints the label it is given; it does not decide, map,
|
|
10
|
+
translate or shorten a status.
|
|
11
|
+
-->
|
|
12
|
+
<template>
|
|
13
|
+
<span class="status-chip" :class="[toneClass, { 'status-chip--plain': !dot }]">
|
|
14
|
+
<span v-if="dot" class="status-chip__dot" />
|
|
15
|
+
{{ label ?? status }}
|
|
16
|
+
</span>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { statusToneClass } from "../utils/status";
|
|
21
|
+
|
|
22
|
+
const props = defineProps({
|
|
23
|
+
/** The status word - what decides the colour. */
|
|
24
|
+
status: { type: String, default: "" },
|
|
25
|
+
/** What to PRINT, when that differs from the word being matched on. */
|
|
26
|
+
label: { type: String, default: undefined },
|
|
27
|
+
/** The design's tag chips (pass IDs, role names) carry no leading dot. */
|
|
28
|
+
dot: { type: Boolean, default: true },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const toneClass = computed(() => statusToneClass(props.status));
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
.status-chip {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: 6px;
|
|
39
|
+
padding: 4px 10px;
|
|
40
|
+
border-radius: var(--r-pill);
|
|
41
|
+
font-family: var(--font-sans);
|
|
42
|
+
font-size: var(--fs-chip-lg);
|
|
43
|
+
font-weight: var(--fw-chip);
|
|
44
|
+
line-height: 1.4;
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
/* `--tone-bg` is set alongside the colour by the `.tone-*` class - see
|
|
47
|
+
assets/css/tokens.css - so one class carries both halves of the pair the
|
|
48
|
+
contrast was measured on. */
|
|
49
|
+
background: var(--tone-bg);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.status-chip--plain {
|
|
53
|
+
border-radius: var(--r-tag);
|
|
54
|
+
font-size: var(--fs-chip);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.status-chip__dot {
|
|
58
|
+
width: 6px;
|
|
59
|
+
height: 6px;
|
|
60
|
+
border-radius: var(--r-pill);
|
|
61
|
+
background: currentColor;
|
|
62
|
+
flex: 0 0 6px;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
package/components/TableMain.vue
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
PHASE 3 - THE STANDARD TABLE CARD.
|
|
3
|
+
|
|
4
|
+
The redesign is, more than anything else, a table card: toolbar row (refresh,
|
|
5
|
+
optional in-card search, pagination) -> uppercase header row -> rows. This is
|
|
6
|
+
the shared one, so this file is where that pattern is drawn once.
|
|
7
|
+
|
|
8
|
+
VISUAL ONLY, and that matters more here than anywhere. Every column, filter,
|
|
9
|
+
sort, action, slot, prop, emit and page still behaves exactly as it did:
|
|
10
|
+
the headers are the caller's, the items are the caller's, every slot is still
|
|
11
|
+
forwarded to `v-data-table` untouched, and no prop's default has changed. The
|
|
12
|
+
empty state is the same wording `v-data-table` already showed, with the icon
|
|
13
|
+
the design draws above it - and it is only supplied when the caller has NOT
|
|
14
|
+
passed its own `no-data` slot.
|
|
15
|
+
-->
|
|
1
16
|
<template>
|
|
2
17
|
<v-row no-gutters>
|
|
3
18
|
<!-- Top Actions -->
|
|
@@ -5,13 +20,17 @@
|
|
|
5
20
|
<v-row no-gutters>
|
|
6
21
|
<slot name="actions">
|
|
7
22
|
<v-col cols="12">
|
|
8
|
-
<v-row no-gutters class="ga-2">
|
|
23
|
+
<v-row no-gutters class="ga-2 align-center table-card__actions">
|
|
24
|
+
<!--
|
|
25
|
+
The design's primary button: the accent fill that may carry a
|
|
26
|
+
white label (`--accent-strong`; white on `--accent` is 3.23:1),
|
|
27
|
+
40px, 10px radius, leading icon. Same click, same label.
|
|
28
|
+
-->
|
|
9
29
|
<v-btn
|
|
10
30
|
v-if="canCreate"
|
|
11
|
-
class="text-none"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
size="large"
|
|
31
|
+
class="text-none table-card__primary"
|
|
32
|
+
variant="flat"
|
|
33
|
+
prepend-icon="mdi-plus"
|
|
15
34
|
@click="emits('create')"
|
|
16
35
|
>
|
|
17
36
|
{{ createLabel }}
|
|
@@ -25,14 +44,14 @@
|
|
|
25
44
|
max-width="300"
|
|
26
45
|
append-inner-icon="mdi-magnify"
|
|
27
46
|
hide-details
|
|
47
|
+
class="table-card__search"
|
|
28
48
|
/>
|
|
49
|
+
<!-- Ghost/secondary, as the design specifies for Scan QR Code. -->
|
|
29
50
|
<v-btn
|
|
30
51
|
v-if="canScanVisitorQRCode"
|
|
31
52
|
text="Scan QR Code"
|
|
32
|
-
class="text-none ml-2"
|
|
33
|
-
|
|
34
|
-
variant="tonal"
|
|
35
|
-
size="large"
|
|
53
|
+
class="text-none ml-2 table-card__ghost"
|
|
54
|
+
variant="outlined"
|
|
36
55
|
@click="emits('scan')"
|
|
37
56
|
/>
|
|
38
57
|
</v-row>
|
|
@@ -45,19 +64,19 @@
|
|
|
45
64
|
<v-col cols="12">
|
|
46
65
|
<v-card
|
|
47
66
|
width="100%"
|
|
48
|
-
variant="
|
|
49
|
-
|
|
50
|
-
rounded="lg"
|
|
67
|
+
variant="flat"
|
|
68
|
+
class="table-card"
|
|
51
69
|
:loading="loading"
|
|
52
70
|
>
|
|
53
71
|
<!-- Toolbar -->
|
|
54
72
|
<v-toolbar
|
|
55
73
|
density="compact"
|
|
56
|
-
color="
|
|
74
|
+
color="transparent"
|
|
75
|
+
class="table-card__toolbar"
|
|
57
76
|
:extension-height="extensionHeight"
|
|
58
77
|
>
|
|
59
78
|
<template #prepend>
|
|
60
|
-
<v-btn fab icon density="comfortable" @click="emits('refresh')">
|
|
79
|
+
<v-btn fab icon density="comfortable" variant="text" @click="emits('refresh')">
|
|
61
80
|
<v-icon>mdi-refresh</v-icon>
|
|
62
81
|
</v-btn>
|
|
63
82
|
<slot name="prepend-additional" />
|
|
@@ -65,7 +84,7 @@
|
|
|
65
84
|
|
|
66
85
|
<template #append>
|
|
67
86
|
<v-row no-gutters justify="end" align="center">
|
|
68
|
-
<span class="mr-2
|
|
87
|
+
<span class="mr-2 table-card__range">
|
|
69
88
|
{{ pageRange }}
|
|
70
89
|
</span>
|
|
71
90
|
<local-pagination
|
|
@@ -96,6 +115,18 @@
|
|
|
96
115
|
<template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
|
|
97
116
|
<slot :name="slotName" v-bind="slotProps" />
|
|
98
117
|
</template>
|
|
118
|
+
|
|
119
|
+
<!--
|
|
120
|
+
The design's empty state. Same sentence the table already showed;
|
|
121
|
+
only supplied when the caller has not passed its own, so nobody's
|
|
122
|
+
existing empty state is replaced.
|
|
123
|
+
-->
|
|
124
|
+
<template v-if="!$slots['no-data']" #no-data>
|
|
125
|
+
<div class="table-card__empty">
|
|
126
|
+
<v-icon icon="mdi-magnify-close" size="32" />
|
|
127
|
+
<span>No data available</span>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
99
130
|
</v-data-table>
|
|
100
131
|
<slot name="footer" />
|
|
101
132
|
</v-card>
|
|
@@ -187,7 +218,134 @@ watch(
|
|
|
187
218
|
</script>
|
|
188
219
|
|
|
189
220
|
<style scoped>
|
|
190
|
-
|
|
191
|
-
|
|
221
|
+
/* ------------------------------------------------------------------ */
|
|
222
|
+
/* The card. */
|
|
223
|
+
/* ------------------------------------------------------------------ */
|
|
224
|
+
|
|
225
|
+
.table-card {
|
|
226
|
+
font-family: var(--font-sans);
|
|
227
|
+
background: var(--card);
|
|
228
|
+
border: 1px solid var(--border);
|
|
229
|
+
border-radius: var(--r-card);
|
|
230
|
+
box-shadow: var(--shadow);
|
|
231
|
+
overflow: hidden;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The toolbar was `color="grey-lighten-4"` - a literal Vuetify grey, which is
|
|
236
|
+
* a near-white bar sitting on a #1c1e24 card in dark mode. It is the card's
|
|
237
|
+
* own surface now, with the design's rule under it.
|
|
238
|
+
*/
|
|
239
|
+
.table-card__toolbar {
|
|
240
|
+
border-bottom: 1px solid var(--border);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.table-card__range {
|
|
244
|
+
font-size: 12px;
|
|
245
|
+
font-weight: 600;
|
|
246
|
+
color: var(--muted);
|
|
247
|
+
font-variant-numeric: tabular-nums;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* ------------------------------------------------------------------ */
|
|
251
|
+
/* The header row and the rows under it. */
|
|
252
|
+
/* ------------------------------------------------------------------ */
|
|
253
|
+
|
|
254
|
+
.table-card :deep(thead th) {
|
|
255
|
+
background: var(--thead) !important;
|
|
256
|
+
color: var(--muted) !important;
|
|
257
|
+
font-size: var(--fs-table-head) !important;
|
|
258
|
+
font-weight: var(--fw-table-head) !important;
|
|
259
|
+
letter-spacing: var(--ls-table-head);
|
|
260
|
+
text-transform: uppercase;
|
|
261
|
+
padding: 10px var(--cellpad-x) !important;
|
|
262
|
+
height: auto !important;
|
|
263
|
+
border-bottom: 1px solid var(--border) !important;
|
|
264
|
+
white-space: nowrap;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.table-card :deep(tbody td) {
|
|
268
|
+
font-size: var(--fs-cell);
|
|
269
|
+
font-weight: var(--fw-cell);
|
|
270
|
+
color: var(--text);
|
|
271
|
+
padding: var(--cellpad) var(--cellpad-x) !important;
|
|
272
|
+
height: auto !important;
|
|
273
|
+
font-variant-numeric: tabular-nums;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.table-card :deep(tbody tr:hover > td) {
|
|
277
|
+
background: var(--hover);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* ------------------------------------------------------------------ */
|
|
281
|
+
/* Empty state, in-card search, buttons and in-card tabs. */
|
|
282
|
+
/* ------------------------------------------------------------------ */
|
|
283
|
+
|
|
284
|
+
.table-card__empty {
|
|
285
|
+
display: flex;
|
|
286
|
+
flex-direction: column;
|
|
287
|
+
align-items: center;
|
|
288
|
+
justify-content: center;
|
|
289
|
+
gap: 8px;
|
|
290
|
+
padding: 52px 16px;
|
|
291
|
+
font-size: var(--fs-cell);
|
|
292
|
+
color: var(--muted);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.table-card__empty :deep(.v-icon) {
|
|
296
|
+
color: var(--muted);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* The in-card variant: 34px, 10px radius, as drawn. */
|
|
300
|
+
.table-card__search :deep(.v-field) {
|
|
301
|
+
border-radius: var(--r-inner);
|
|
302
|
+
font-size: var(--fs-cell);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.table-card__primary {
|
|
306
|
+
height: var(--btn-h);
|
|
307
|
+
border-radius: var(--r-inner);
|
|
308
|
+
padding: 0 16px;
|
|
309
|
+
font-size: var(--fs-btn);
|
|
310
|
+
font-weight: var(--fw-btn-strong);
|
|
311
|
+
letter-spacing: 0;
|
|
312
|
+
background: var(--accent-strong);
|
|
313
|
+
color: var(--on-accent-strong);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.table-card__ghost {
|
|
317
|
+
height: var(--btn-h);
|
|
318
|
+
border-radius: var(--r-inner);
|
|
319
|
+
padding: 0 16px;
|
|
320
|
+
font-size: var(--fs-btn);
|
|
321
|
+
font-weight: var(--fw-btn);
|
|
322
|
+
letter-spacing: 0;
|
|
323
|
+
color: var(--text2);
|
|
324
|
+
border-color: var(--border);
|
|
325
|
+
background: var(--card);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Tabs that a screen puts in this card's extension slot: 13px, the active one
|
|
330
|
+
* at 800 in `--accent-text` over a 2px accent underline. Tabs elsewhere in the
|
|
331
|
+
* product are their own screens' and are restyled with them.
|
|
332
|
+
*/
|
|
333
|
+
.table-card :deep(.v-tab) {
|
|
334
|
+
font-size: var(--fs-nav);
|
|
335
|
+
font-weight: var(--fw-cell);
|
|
336
|
+
letter-spacing: 0;
|
|
337
|
+
text-transform: none;
|
|
338
|
+
color: var(--muted);
|
|
339
|
+
min-width: 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.table-card :deep(.v-tab.v-tab--selected) {
|
|
343
|
+
font-weight: var(--fw-table-head);
|
|
344
|
+
color: var(--accent-text);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.table-card :deep(.v-tab .v-tab__slider) {
|
|
348
|
+
height: 2px;
|
|
349
|
+
background: var(--accent);
|
|
192
350
|
}
|
|
193
351
|
</style>
|
package/composables/useRole.ts
CHANGED
|
@@ -68,6 +68,26 @@ export default function useRole() {
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function getDeletionPreview(_id: string) {
|
|
72
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
73
|
+
`/api/roles/${_id}/deletion-preview`,
|
|
74
|
+
{ method: "GET" }
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function deleteWithReassignments(
|
|
79
|
+
_id: string,
|
|
80
|
+
reassignments: Array<{ memberId: string; roleId: string }>
|
|
81
|
+
) {
|
|
82
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
83
|
+
`/api/roles/${_id}/delete-with-reassignments`,
|
|
84
|
+
{
|
|
85
|
+
method: "PUT",
|
|
86
|
+
body: { reassignments },
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
const role = useState<TRole>("userRole", () => {
|
|
72
92
|
return {
|
|
73
93
|
_id: "",
|
|
@@ -103,6 +123,8 @@ export default function useRole() {
|
|
|
103
123
|
updateRoleById,
|
|
104
124
|
updateRoleFieldById,
|
|
105
125
|
deleteRole,
|
|
126
|
+
getDeletionPreview,
|
|
127
|
+
deleteWithReassignments,
|
|
106
128
|
updatePermissionById,
|
|
107
129
|
getRoleByUserId,
|
|
108
130
|
};
|