@eeplatform/nuxt-layer-common 1.7.47 → 1.7.49
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 +12 -0
- package/components/Layout/Header.vue +5 -5
- package/components/ListGroupSelection.vue +134 -0
- package/components/RoleForm.vue +327 -0
- package/components/RolePermissionMain.vue +131 -199
- package/composables/useApps.ts +58 -0
- package/composables/useLocal.ts +1 -1
- package/composables/useLocalAuth.ts +15 -0
- package/composables/useMember.ts +63 -33
- package/composables/usePermission.ts +97 -0
- package/composables/useRole.ts +24 -37
- package/middleware/02.member.ts +8 -4
- package/middleware/03.role.ts +4 -2
- package/package.json +1 -1
- package/plugins/secure-member.client.ts +2 -2
- package/types/local.d.ts +8 -4
- package/types/member.d.ts +9 -0
- package/types/permission.d.ts +24 -0
- package/types/role.d.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
|
|
80
80
|
<v-row no-gutters class="px-2 pb-2">
|
|
81
81
|
<v-col
|
|
82
|
-
v-for="item in
|
|
82
|
+
v-for="item in apps"
|
|
83
83
|
:key="item.title"
|
|
84
84
|
cols="4"
|
|
85
85
|
class="pa-2"
|
|
@@ -206,7 +206,7 @@ const props = defineProps({
|
|
|
206
206
|
default: "Title",
|
|
207
207
|
},
|
|
208
208
|
defaults: {
|
|
209
|
-
type: Array as PropType<
|
|
209
|
+
type: Array as PropType<Record<string, any>[]>,
|
|
210
210
|
default: () => [
|
|
211
211
|
{
|
|
212
212
|
title: "Account",
|
|
@@ -223,11 +223,11 @@ const props = defineProps({
|
|
|
223
223
|
],
|
|
224
224
|
},
|
|
225
225
|
apps: {
|
|
226
|
-
type: Array as PropType<
|
|
226
|
+
type: Array as PropType<Record<string, any>[]>,
|
|
227
227
|
default: () => [],
|
|
228
228
|
},
|
|
229
229
|
others: {
|
|
230
|
-
type: Array as PropType<
|
|
230
|
+
type: Array as PropType<Record<string, any>[]>,
|
|
231
231
|
default: () => [],
|
|
232
232
|
},
|
|
233
233
|
});
|
|
@@ -244,7 +244,7 @@ function setSearchValue() {
|
|
|
244
244
|
search.value = _search.value;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
const { redirect, drawer } = useLocal();
|
|
247
|
+
const { redirect, drawer, apps } = useLocal();
|
|
248
248
|
|
|
249
249
|
const { APP_NAME, APP_NAME_ROUTE } = useRuntimeConfig().public;
|
|
250
250
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card width="100%" v-bind="attrs">
|
|
3
|
+
<v-list
|
|
4
|
+
v-model:selected="selected"
|
|
5
|
+
lines="two"
|
|
6
|
+
:select-strategy="attrs.readonly ? 'classic' : 'leaf'"
|
|
7
|
+
class="pa-0"
|
|
8
|
+
density="compact"
|
|
9
|
+
read-only
|
|
10
|
+
open-strategy="single"
|
|
11
|
+
>
|
|
12
|
+
<template name="prepend"></template>
|
|
13
|
+
|
|
14
|
+
<template
|
|
15
|
+
v-for="(keyGroup, keyGroupIndex) in props.items"
|
|
16
|
+
:key="keyGroupIndex"
|
|
17
|
+
>
|
|
18
|
+
<v-divider v-if="keyGroupIndex > 0"></v-divider>
|
|
19
|
+
<v-list-group :value="keyGroup.title" fluid>
|
|
20
|
+
<template v-slot:activator="{ props }">
|
|
21
|
+
<v-list-item v-bind="props" density="compact">
|
|
22
|
+
<span class="text-capitalize">
|
|
23
|
+
{{ keyGroup.title }}
|
|
24
|
+
</span>
|
|
25
|
+
|
|
26
|
+
<template #prepend>
|
|
27
|
+
<v-chip class="mr-2" small>
|
|
28
|
+
{{ selectedActionCount(String(keyGroup.key)) }}
|
|
29
|
+
</v-chip>
|
|
30
|
+
</template>
|
|
31
|
+
</v-list-item>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<template
|
|
35
|
+
v-for="(item, itemIndex) in keyGroup.children"
|
|
36
|
+
:key="itemIndex"
|
|
37
|
+
>
|
|
38
|
+
<v-divider></v-divider>
|
|
39
|
+
<v-list-item v-if="attrs.readonly" density="compact">
|
|
40
|
+
<template #title>
|
|
41
|
+
<span class="text-subtitle-2 text-capitalize pl-11">
|
|
42
|
+
{{ item.title }}
|
|
43
|
+
</span>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<template #subtitle>
|
|
47
|
+
<span class="text-subtitle-2 pl-11">{{
|
|
48
|
+
item.description
|
|
49
|
+
}}</span>
|
|
50
|
+
</template>
|
|
51
|
+
</v-list-item>
|
|
52
|
+
|
|
53
|
+
<v-list-item v-else :value="item.key" density="compact">
|
|
54
|
+
<template #title>
|
|
55
|
+
<span class="text-subtitle-2 text-capitalize">
|
|
56
|
+
{{ item.title }}
|
|
57
|
+
</span>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<template #subtitle>
|
|
61
|
+
<span class="text-subtitle-2 text-capitalize">
|
|
62
|
+
{{ item.description }}
|
|
63
|
+
</span>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<template #prepend="{ isSelected }">
|
|
67
|
+
<v-list-item-action start class="pl-1">
|
|
68
|
+
<v-checkbox-btn
|
|
69
|
+
:model-value="isSelected"
|
|
70
|
+
readonly
|
|
71
|
+
@click="addItem(item.key)"
|
|
72
|
+
></v-checkbox-btn>
|
|
73
|
+
</v-list-item-action>
|
|
74
|
+
</template>
|
|
75
|
+
</v-list-item>
|
|
76
|
+
</template>
|
|
77
|
+
</v-list-group>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<slot v-if="noData" name="no-data">
|
|
81
|
+
<v-list-item>
|
|
82
|
+
<v-list-item-title>No data</v-list-item-title>
|
|
83
|
+
</v-list-item>
|
|
84
|
+
</slot>
|
|
85
|
+
|
|
86
|
+
<template name="append"></template>
|
|
87
|
+
</v-list>
|
|
88
|
+
</v-card>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
const selected = defineModel<Array<string>>({ default: [] });
|
|
93
|
+
const attrs = useAttrs();
|
|
94
|
+
|
|
95
|
+
function addItem(key: string) {
|
|
96
|
+
if (selected.value.includes(key)) {
|
|
97
|
+
// remove item from selected without reassigning the array
|
|
98
|
+
const index = selected.value.indexOf(key);
|
|
99
|
+
if (index > -1) {
|
|
100
|
+
selected.value.splice(index, 1);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
selected.value.push(key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type TListGroup = {
|
|
108
|
+
title: string;
|
|
109
|
+
key: string;
|
|
110
|
+
children: { title: string; key: string; description: string }[];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const props = defineProps({
|
|
114
|
+
items: {
|
|
115
|
+
// Array of object
|
|
116
|
+
type: Array as PropType<TListGroup[]>,
|
|
117
|
+
required: true,
|
|
118
|
+
default: () => [],
|
|
119
|
+
},
|
|
120
|
+
message: {
|
|
121
|
+
type: String,
|
|
122
|
+
default: "",
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const selectedActionCount = (resource: string) => {
|
|
127
|
+
return selected.value.filter((permission) => permission.startsWith(resource))
|
|
128
|
+
.length;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const noData = computed(() => {
|
|
132
|
+
return Object.keys(props.items).length === 0;
|
|
133
|
+
});
|
|
134
|
+
</script>
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card width="100%">
|
|
3
|
+
<v-toolbar>
|
|
4
|
+
<v-row no-gutters class="fill-height px-6" align="center">
|
|
5
|
+
<span class="font-weight-bold text-h5 text-capitalize">
|
|
6
|
+
{{ localProps.title }}
|
|
7
|
+
</span>
|
|
8
|
+
</v-row>
|
|
9
|
+
</v-toolbar>
|
|
10
|
+
|
|
11
|
+
<v-card-text style="max-height: 100vh; overflow-y: auto">
|
|
12
|
+
<v-form v-model="valid">
|
|
13
|
+
<v-row no-gutters>
|
|
14
|
+
<v-col v-if="isOrg" cols="12" class="mb-1">
|
|
15
|
+
<v-row no-gutters>
|
|
16
|
+
<InputLabel
|
|
17
|
+
class="text-capitalize"
|
|
18
|
+
title="App"
|
|
19
|
+
:required="isMutable"
|
|
20
|
+
/>
|
|
21
|
+
<v-col cols="12">
|
|
22
|
+
<v-select
|
|
23
|
+
v-model="role.app"
|
|
24
|
+
:items="apps"
|
|
25
|
+
item-title="name"
|
|
26
|
+
item-value="code"
|
|
27
|
+
:readonly="!isMutable"
|
|
28
|
+
:loading="loadingApps"
|
|
29
|
+
></v-select>
|
|
30
|
+
</v-col>
|
|
31
|
+
</v-row>
|
|
32
|
+
</v-col>
|
|
33
|
+
|
|
34
|
+
<v-col cols="12" class="mb-1">
|
|
35
|
+
<v-row no-gutters>
|
|
36
|
+
<InputLabel
|
|
37
|
+
class="text-capitalize"
|
|
38
|
+
title="Name"
|
|
39
|
+
:required="isMutable"
|
|
40
|
+
/>
|
|
41
|
+
<v-col cols="12">
|
|
42
|
+
<v-text-field
|
|
43
|
+
v-model="role.name"
|
|
44
|
+
:rules="isMutable ? [requiredRule] : []"
|
|
45
|
+
:readonly="!isMutable"
|
|
46
|
+
></v-text-field>
|
|
47
|
+
</v-col>
|
|
48
|
+
</v-row>
|
|
49
|
+
</v-col>
|
|
50
|
+
|
|
51
|
+
<v-col cols="12" class="mb-1">
|
|
52
|
+
<v-row no-gutters>
|
|
53
|
+
<InputLabel
|
|
54
|
+
class="text-capitalize"
|
|
55
|
+
title="Permissions"
|
|
56
|
+
:required="isMutable"
|
|
57
|
+
/>
|
|
58
|
+
<v-col cols="12">
|
|
59
|
+
<ListGroupSelection
|
|
60
|
+
v-model="role.permissions"
|
|
61
|
+
:items="permissions"
|
|
62
|
+
:readonly="!isMutable"
|
|
63
|
+
variant="outlined"
|
|
64
|
+
border="thin"
|
|
65
|
+
:loading="loadingPermissions"
|
|
66
|
+
/>
|
|
67
|
+
<v-input
|
|
68
|
+
:messages="errorPermissionSelection"
|
|
69
|
+
:error="!!errorPermissionSelection"
|
|
70
|
+
></v-input>
|
|
71
|
+
</v-col>
|
|
72
|
+
</v-row>
|
|
73
|
+
</v-col>
|
|
74
|
+
|
|
75
|
+
<v-col cols="12" class="mb-1">
|
|
76
|
+
<v-row no-gutters>
|
|
77
|
+
<InputLabel class="text-capitalize" title="Description" />
|
|
78
|
+
<v-col cols="12">
|
|
79
|
+
<v-textarea
|
|
80
|
+
v-model="role.description"
|
|
81
|
+
rows="2"
|
|
82
|
+
no-resize
|
|
83
|
+
:readonly="!isMutable"
|
|
84
|
+
></v-textarea>
|
|
85
|
+
</v-col>
|
|
86
|
+
</v-row>
|
|
87
|
+
</v-col>
|
|
88
|
+
</v-row>
|
|
89
|
+
</v-form>
|
|
90
|
+
|
|
91
|
+
<v-alert
|
|
92
|
+
v-if="message"
|
|
93
|
+
type="error"
|
|
94
|
+
variant="flat"
|
|
95
|
+
closable
|
|
96
|
+
position="absolute"
|
|
97
|
+
location="bottom"
|
|
98
|
+
style="bottom: 48px"
|
|
99
|
+
@click:close="message = ''"
|
|
100
|
+
width="100%"
|
|
101
|
+
tile
|
|
102
|
+
class="text-caption"
|
|
103
|
+
>
|
|
104
|
+
{{ message }}
|
|
105
|
+
</v-alert>
|
|
106
|
+
</v-card-text>
|
|
107
|
+
|
|
108
|
+
<v-toolbar density="compact">
|
|
109
|
+
<v-row no-gutters>
|
|
110
|
+
<v-col v-if="isMutable" cols="6">
|
|
111
|
+
<v-btn
|
|
112
|
+
tile
|
|
113
|
+
block
|
|
114
|
+
variant="text"
|
|
115
|
+
class="text-none"
|
|
116
|
+
size="48"
|
|
117
|
+
@click="emits('cancel')"
|
|
118
|
+
>
|
|
119
|
+
Cancel
|
|
120
|
+
</v-btn>
|
|
121
|
+
</v-col>
|
|
122
|
+
|
|
123
|
+
<v-col v-if="localProps.mode === 'view'" cols="6">
|
|
124
|
+
<v-btn
|
|
125
|
+
tile
|
|
126
|
+
block
|
|
127
|
+
variant="text"
|
|
128
|
+
class="text-none"
|
|
129
|
+
size="48"
|
|
130
|
+
@click="emits('close')"
|
|
131
|
+
>
|
|
132
|
+
Close
|
|
133
|
+
</v-btn>
|
|
134
|
+
</v-col>
|
|
135
|
+
|
|
136
|
+
<v-col v-if="localProps.mode === 'view'" cols="6">
|
|
137
|
+
<v-menu>
|
|
138
|
+
<template #activator="{ props }">
|
|
139
|
+
<v-btn
|
|
140
|
+
block
|
|
141
|
+
variant="flat"
|
|
142
|
+
color="black"
|
|
143
|
+
class="text-none"
|
|
144
|
+
height="48"
|
|
145
|
+
v-bind="props"
|
|
146
|
+
tile
|
|
147
|
+
>
|
|
148
|
+
More actions
|
|
149
|
+
</v-btn>
|
|
150
|
+
</template>
|
|
151
|
+
|
|
152
|
+
<v-list class="pa-0">
|
|
153
|
+
<v-list-item @click="emits('edit')">
|
|
154
|
+
<v-list-item-title class="text-subtitle-2">
|
|
155
|
+
Edit Role
|
|
156
|
+
</v-list-item-title>
|
|
157
|
+
</v-list-item>
|
|
158
|
+
|
|
159
|
+
<v-list-item @click="emits('delete')" class="text-red">
|
|
160
|
+
<v-list-item-title class="text-subtitle-2">
|
|
161
|
+
Delete Role
|
|
162
|
+
</v-list-item-title>
|
|
163
|
+
</v-list-item>
|
|
164
|
+
</v-list>
|
|
165
|
+
</v-menu>
|
|
166
|
+
</v-col>
|
|
167
|
+
|
|
168
|
+
<v-col v-if="isMutable" cols="6">
|
|
169
|
+
<v-btn
|
|
170
|
+
tile
|
|
171
|
+
block
|
|
172
|
+
variant="flat"
|
|
173
|
+
color="black"
|
|
174
|
+
class="text-none"
|
|
175
|
+
size="48"
|
|
176
|
+
@click="emits('submit')"
|
|
177
|
+
:disabled="!valid"
|
|
178
|
+
>
|
|
179
|
+
{{ localProps.mode === "add" ? "Add Role" : "Save Changes" }}
|
|
180
|
+
</v-btn>
|
|
181
|
+
</v-col>
|
|
182
|
+
</v-row>
|
|
183
|
+
</v-toolbar>
|
|
184
|
+
</v-card>
|
|
185
|
+
</template>
|
|
186
|
+
|
|
187
|
+
<script setup lang="ts">
|
|
188
|
+
const emits = defineEmits(["submit", "cancel", "close", "edit", "delete"]);
|
|
189
|
+
|
|
190
|
+
const isMutable = computed(() => ["add", "edit"].includes(localProps.mode));
|
|
191
|
+
|
|
192
|
+
const localProps = defineProps({
|
|
193
|
+
title: {
|
|
194
|
+
type: String,
|
|
195
|
+
default: "Role Form",
|
|
196
|
+
},
|
|
197
|
+
app: {
|
|
198
|
+
type: String,
|
|
199
|
+
default: "",
|
|
200
|
+
},
|
|
201
|
+
mode: {
|
|
202
|
+
type: String,
|
|
203
|
+
default: "add",
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const role = defineModel<TRole>({
|
|
208
|
+
default: () => useRole().role.value,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const errorPermissionSelection = computed(() =>
|
|
212
|
+
requireListRule(role.value.permissions ?? [])
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const message = defineModel("message", { default: "" });
|
|
216
|
+
|
|
217
|
+
const valid = ref(false);
|
|
218
|
+
|
|
219
|
+
const { requiredRule, requireListRule } = useUtils();
|
|
220
|
+
|
|
221
|
+
const apps = ref<TApp[]>([]);
|
|
222
|
+
|
|
223
|
+
const { getAll: getAllApps } = useApps();
|
|
224
|
+
|
|
225
|
+
const isOrg = computed(() => localProps.app === "org");
|
|
226
|
+
|
|
227
|
+
const {
|
|
228
|
+
data: appsData,
|
|
229
|
+
status: appsReqStatus,
|
|
230
|
+
refresh: refreshApps,
|
|
231
|
+
} = await useLazyAsyncData(
|
|
232
|
+
`role-form-get-apps-${localProps.app}`,
|
|
233
|
+
() =>
|
|
234
|
+
getAllApps({
|
|
235
|
+
limit: 20,
|
|
236
|
+
}),
|
|
237
|
+
{
|
|
238
|
+
immediate: isOrg.value,
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const loadingApps = computed(() => appsReqStatus.value === "pending");
|
|
243
|
+
|
|
244
|
+
watchEffect(() => {
|
|
245
|
+
if (appsData.value) {
|
|
246
|
+
apps.value = appsData.value.items;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
type PermChild = {
|
|
251
|
+
title: string;
|
|
252
|
+
key: string;
|
|
253
|
+
description: string;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
type PermGroup = {
|
|
257
|
+
title: string;
|
|
258
|
+
key: string;
|
|
259
|
+
children: PermChild[];
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const permissions = ref<PermGroup[]>([]);
|
|
263
|
+
|
|
264
|
+
const { getAllPerm } = usePermission();
|
|
265
|
+
|
|
266
|
+
const {
|
|
267
|
+
data: permissionsData,
|
|
268
|
+
status: permissionsReqStatus,
|
|
269
|
+
refresh: refreshPermissions,
|
|
270
|
+
} = await useLazyAsyncData(
|
|
271
|
+
`role-form-get-all-permission-${role.value.app}`,
|
|
272
|
+
() =>
|
|
273
|
+
getAllPerm({
|
|
274
|
+
app: role.value.app,
|
|
275
|
+
limit: 200,
|
|
276
|
+
}),
|
|
277
|
+
{
|
|
278
|
+
watch: [() => role.value.app],
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const loadingPermissions = computed(
|
|
283
|
+
() => permissionsReqStatus.value === "pending"
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
function groupPermissions(permissions: TPerm[]): PermGroup[] {
|
|
287
|
+
const groups: Record<string, PermGroup> = {};
|
|
288
|
+
|
|
289
|
+
for (const perm of permissions) {
|
|
290
|
+
if (perm.status !== "active") continue;
|
|
291
|
+
|
|
292
|
+
const groupKey = perm.group;
|
|
293
|
+
|
|
294
|
+
if (!groups[groupKey]) {
|
|
295
|
+
groups[groupKey] = {
|
|
296
|
+
title: groupKey,
|
|
297
|
+
key: groupKey,
|
|
298
|
+
children: [],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
groups[groupKey].children.push({
|
|
303
|
+
title: perm.name,
|
|
304
|
+
key: perm.key,
|
|
305
|
+
description: perm.description,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return Object.values(groups);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
watchEffect(() => {
|
|
313
|
+
if (
|
|
314
|
+
permissionsData.value &&
|
|
315
|
+
permissionsData.value &&
|
|
316
|
+
permissionsData.value.items
|
|
317
|
+
) {
|
|
318
|
+
let items = permissionsData.value.items;
|
|
319
|
+
if (localProps.mode === "view") {
|
|
320
|
+
items = permissionsData.value.items.filter((p: TPerm) =>
|
|
321
|
+
role.value.permissions?.includes(p.key)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
permissions.value = groupPermissions(items);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
</script>
|