@fishawack/lab-velocity 2.0.0-beta.30 → 2.0.0-beta.32
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/_Build/vue/components/form/Checkbox.vue +10 -0
- package/_Build/vue/components/form/Select.vue +229 -26
- package/_Build/vue/components/layout/Audit.vue +75 -0
- package/_Build/vue/modules/AuthModule/routes/PCompanies/resource.js +16 -2
- package/_Build/vue/modules/AuthModule/routes/PUsers/resource.js +94 -28
- package/_Build/vue/modules/resource/Children/create.vue +12 -1
- package/_Build/vue/modules/resource/Children/edit.vue +19 -5
- package/_Build/vue/modules/resource/Children/partials/form.vue +2 -0
- package/_Build/vue/modules/resource/Children/show.vue +10 -3
- package/_Build/vue/modules/resource/index.js +91 -74
- package/components/_datepicker.scss +1 -0
- package/index.js +1 -0
- package/package.json +2 -2
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
:disabled="disabled"
|
|
7
7
|
v-model="content"
|
|
8
8
|
:required="required"
|
|
9
|
+
:true-label="trueValue"
|
|
10
|
+
:false-label="falseValue"
|
|
9
11
|
@change="handleInput"
|
|
10
12
|
>
|
|
11
13
|
</el-checkbox>
|
|
@@ -29,6 +31,14 @@ export default {
|
|
|
29
31
|
type: String,
|
|
30
32
|
default: "vel-checkbox",
|
|
31
33
|
},
|
|
34
|
+
trueValue: {
|
|
35
|
+
type: [String, Number, Boolean],
|
|
36
|
+
default: true,
|
|
37
|
+
},
|
|
38
|
+
falseValue: {
|
|
39
|
+
type: [String, Number, Boolean],
|
|
40
|
+
default: false,
|
|
41
|
+
},
|
|
32
42
|
},
|
|
33
43
|
|
|
34
44
|
components: {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
</template>
|
|
6
6
|
|
|
7
7
|
<el-select
|
|
8
|
+
ref="select"
|
|
8
9
|
v-model="content"
|
|
9
10
|
:class="baseClass"
|
|
10
11
|
:multiple="multiple"
|
|
@@ -14,32 +15,42 @@
|
|
|
14
15
|
:collapse-tags="collapseTags"
|
|
15
16
|
:filterable="filterable"
|
|
16
17
|
:value-on-clear="null"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
remote-show-suffix
|
|
19
|
+
:remote="!!endpoint"
|
|
20
|
+
:remote-method="endpoint ? handleSearch : undefined"
|
|
21
|
+
:loading="initialLoading"
|
|
20
22
|
:empty-values="[null, undefined]"
|
|
23
|
+
@change="handleInput"
|
|
24
|
+
@clear="$emit('clear')"
|
|
25
|
+
@blur="$emit('blur')"
|
|
26
|
+
@visible-change="handleVisibleChange"
|
|
21
27
|
>
|
|
22
28
|
<template #default>
|
|
23
29
|
<slot name="default">
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
<template v-if="!currentOptions[0]?.label">
|
|
31
|
+
<el-option
|
|
32
|
+
v-for="(label, value) in currentOptions"
|
|
33
|
+
:key="value"
|
|
34
|
+
:label="label"
|
|
35
|
+
:value="castValue(value)"
|
|
36
|
+
>
|
|
37
|
+
</el-option>
|
|
38
|
+
</template>
|
|
39
|
+
<template v-else>
|
|
40
|
+
<el-option
|
|
41
|
+
v-for="option in currentOptions"
|
|
42
|
+
:key="option.value"
|
|
43
|
+
:label="option.label"
|
|
44
|
+
:disabled="option.disabled"
|
|
45
|
+
:value="option"
|
|
46
|
+
>
|
|
47
|
+
</el-option>
|
|
48
|
+
</template>
|
|
41
49
|
</slot>
|
|
42
50
|
</template>
|
|
51
|
+
<template v-if="endpoint" #loading>
|
|
52
|
+
<div class="el-select-dropdown__loading">Loading...</div>
|
|
53
|
+
</template>
|
|
43
54
|
</el-select>
|
|
44
55
|
</XInput>
|
|
45
56
|
</template>
|
|
@@ -47,12 +58,21 @@
|
|
|
47
58
|
<script>
|
|
48
59
|
import { ElSelect } from "element-plus";
|
|
49
60
|
import { ElOption } from "element-plus";
|
|
61
|
+
import { ElNotification } from "element-plus";
|
|
50
62
|
import input from "./input.js";
|
|
51
63
|
import XInput from "./input.vue";
|
|
52
64
|
import _ from "lodash";
|
|
65
|
+
import axios from "axios";
|
|
53
66
|
|
|
54
67
|
export default {
|
|
68
|
+
components: {
|
|
69
|
+
XInput,
|
|
70
|
+
ElOption,
|
|
71
|
+
ElSelect,
|
|
72
|
+
},
|
|
73
|
+
|
|
55
74
|
mixins: [input],
|
|
75
|
+
|
|
56
76
|
props: {
|
|
57
77
|
...input.props,
|
|
58
78
|
modelValue: {
|
|
@@ -77,21 +97,123 @@ export default {
|
|
|
77
97
|
},
|
|
78
98
|
options: {
|
|
79
99
|
type: Array,
|
|
80
|
-
default: [],
|
|
100
|
+
default: () => [],
|
|
81
101
|
},
|
|
82
102
|
multiple: {
|
|
83
103
|
type: Boolean,
|
|
84
104
|
default: false,
|
|
85
105
|
},
|
|
106
|
+
endpoint: {
|
|
107
|
+
type: String,
|
|
108
|
+
default: null,
|
|
109
|
+
},
|
|
110
|
+
params: {
|
|
111
|
+
type: Object,
|
|
112
|
+
default: () => ({}),
|
|
113
|
+
},
|
|
114
|
+
labelKey: {
|
|
115
|
+
type: String,
|
|
116
|
+
default: "label",
|
|
117
|
+
},
|
|
118
|
+
valueKey: {
|
|
119
|
+
type: String,
|
|
120
|
+
default: "id",
|
|
121
|
+
},
|
|
122
|
+
searchParam: {
|
|
123
|
+
type: String,
|
|
124
|
+
default: "name",
|
|
125
|
+
},
|
|
126
|
+
perPage: {
|
|
127
|
+
type: Number,
|
|
128
|
+
default: 10,
|
|
129
|
+
},
|
|
86
130
|
},
|
|
87
131
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
132
|
+
emits: ["change", "clear", "blur"],
|
|
133
|
+
|
|
134
|
+
data() {
|
|
135
|
+
return {
|
|
136
|
+
initialLoading: false,
|
|
137
|
+
loadingMore: false,
|
|
138
|
+
asyncOptions: [],
|
|
139
|
+
currentPage: 1,
|
|
140
|
+
hasMore: false,
|
|
141
|
+
searchQuery: "",
|
|
142
|
+
lastSearchQuery: "",
|
|
143
|
+
initialLoadDone: false,
|
|
144
|
+
scrollListener: null,
|
|
145
|
+
wrapScrollTop: 0,
|
|
146
|
+
wrapScrollLeft: 0,
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
computed: {
|
|
151
|
+
currentOptions() {
|
|
152
|
+
// If endpoint is provided, use async options, otherwise use static options
|
|
153
|
+
if (this.endpoint) {
|
|
154
|
+
return this.asyncOptions;
|
|
155
|
+
}
|
|
156
|
+
return this.options;
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
mounted() {
|
|
161
|
+
if (this.endpoint && this.$refs.select?.$refs?.scrollbarRef) {
|
|
162
|
+
const scrollbar = this.$refs.select.$refs.scrollbarRef;
|
|
163
|
+
const wrapRef = scrollbar.wrapRef;
|
|
164
|
+
|
|
165
|
+
this.scrollListener = () => {
|
|
166
|
+
if (!wrapRef) return;
|
|
167
|
+
|
|
168
|
+
const distance = 10;
|
|
169
|
+
const prevTop = this.wrapScrollTop;
|
|
170
|
+
const prevLeft = this.wrapScrollLeft;
|
|
171
|
+
|
|
172
|
+
this.wrapScrollTop = wrapRef.scrollTop;
|
|
173
|
+
this.wrapScrollLeft = wrapRef.scrollLeft;
|
|
174
|
+
|
|
175
|
+
const arrivedStates = {
|
|
176
|
+
bottom:
|
|
177
|
+
this.wrapScrollTop + wrapRef.clientHeight >=
|
|
178
|
+
wrapRef.scrollHeight - distance,
|
|
179
|
+
top: this.wrapScrollTop <= distance && prevTop !== 0,
|
|
180
|
+
right:
|
|
181
|
+
this.wrapScrollLeft + wrapRef.clientWidth >=
|
|
182
|
+
wrapRef.scrollWidth - distance &&
|
|
183
|
+
prevLeft !== this.wrapScrollLeft,
|
|
184
|
+
left: this.wrapScrollLeft <= distance && prevLeft !== 0,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
let direction = null;
|
|
188
|
+
if (prevTop !== this.wrapScrollTop) {
|
|
189
|
+
direction = this.wrapScrollTop > prevTop ? "bottom" : "top";
|
|
190
|
+
}
|
|
191
|
+
if (prevLeft !== this.wrapScrollLeft) {
|
|
192
|
+
direction =
|
|
193
|
+
this.wrapScrollLeft > prevLeft ? "right" : "left";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (direction && arrivedStates[direction]) {
|
|
197
|
+
this.handleEndReached(direction);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
wrapRef.addEventListener("scroll", this.scrollListener);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
beforeUnmount() {
|
|
206
|
+
if (
|
|
207
|
+
this.scrollListener &&
|
|
208
|
+
this.$refs.select?.$refs?.scrollbarRef?.wrapRef
|
|
209
|
+
) {
|
|
210
|
+
this.$refs.select.$refs.scrollbarRef.wrapRef.removeEventListener(
|
|
211
|
+
"scroll",
|
|
212
|
+
this.scrollListener,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
92
215
|
},
|
|
93
216
|
|
|
94
|
-
emits: ["change", "clear", "blur"],
|
|
95
217
|
methods: {
|
|
96
218
|
castValue(value) {
|
|
97
219
|
if (
|
|
@@ -104,6 +226,87 @@ export default {
|
|
|
104
226
|
}
|
|
105
227
|
return value;
|
|
106
228
|
},
|
|
229
|
+
|
|
230
|
+
async handleVisibleChange(visible) {
|
|
231
|
+
// Load data when dropdown is opened for the first time
|
|
232
|
+
if (visible && this.endpoint && !this.initialLoadDone) {
|
|
233
|
+
await this.fetchOptions(1, "");
|
|
234
|
+
this.initialLoadDone = true;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
async handleSearch(query) {
|
|
239
|
+
if (!this.endpoint) return;
|
|
240
|
+
if (query === this.lastSearchQuery) return;
|
|
241
|
+
|
|
242
|
+
this.searchQuery = query;
|
|
243
|
+
this.lastSearchQuery = query;
|
|
244
|
+
this.currentPage = 1;
|
|
245
|
+
this.asyncOptions = [];
|
|
246
|
+
await this.fetchOptions(1, query);
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
handleEndReached(direction) {
|
|
250
|
+
if (!this.endpoint) return;
|
|
251
|
+
if (direction !== "bottom") return;
|
|
252
|
+
if (!this.hasMore || this.loadingMore) return;
|
|
253
|
+
this.fetchOptions(this.currentPage + 1, this.searchQuery);
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
async fetchOptions(page, searchQuery = "") {
|
|
257
|
+
const isInitialLoad = page === 1;
|
|
258
|
+
const loadingKey = isInitialLoad ? "initialLoading" : "loadingMore";
|
|
259
|
+
|
|
260
|
+
if (this[loadingKey]) return;
|
|
261
|
+
|
|
262
|
+
this[loadingKey] = true;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const requestParams = {
|
|
266
|
+
...this.params,
|
|
267
|
+
page,
|
|
268
|
+
per_page: this.perPage,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Add search filter if query is provided
|
|
272
|
+
if (searchQuery) {
|
|
273
|
+
requestParams[`filter[${this.searchParam}]`] = searchQuery;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const response = await axios.get(this.endpoint, {
|
|
277
|
+
params: requestParams,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const data = response.data.data || [];
|
|
281
|
+
const meta = response.data.meta;
|
|
282
|
+
|
|
283
|
+
// Transform API data to option format
|
|
284
|
+
const newOptions = data.map((item) => ({
|
|
285
|
+
label: item[this.labelKey],
|
|
286
|
+
value: item[this.valueKey],
|
|
287
|
+
disabled: item.disabled || false,
|
|
288
|
+
}));
|
|
289
|
+
|
|
290
|
+
// If it's the first page, replace options, otherwise append
|
|
291
|
+
if (page === 1) {
|
|
292
|
+
this.asyncOptions = newOptions;
|
|
293
|
+
} else {
|
|
294
|
+
this.asyncOptions = [...this.asyncOptions, ...newOptions];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.currentPage = page;
|
|
298
|
+
this.hasMore = meta && meta.current_page < meta.last_page;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error("Error fetching select options:", error);
|
|
301
|
+
ElNotification.error({
|
|
302
|
+
title: "Error",
|
|
303
|
+
message: "Failed to load options. Please try again.",
|
|
304
|
+
duration: 5000,
|
|
305
|
+
});
|
|
306
|
+
} finally {
|
|
307
|
+
this[loadingKey] = false;
|
|
308
|
+
}
|
|
309
|
+
},
|
|
107
310
|
},
|
|
108
311
|
};
|
|
109
312
|
</script>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-table
|
|
3
|
+
:data="audits"
|
|
4
|
+
:default-sort="
|
|
5
|
+
query && {
|
|
6
|
+
prop: query.sort_by,
|
|
7
|
+
order: query.sort_dir === 'asc' ? 'ascending' : 'descending',
|
|
8
|
+
}
|
|
9
|
+
"
|
|
10
|
+
@sort-change="$emit('sort-change')"
|
|
11
|
+
>
|
|
12
|
+
<el-table-column label="Previous" :fit="false">
|
|
13
|
+
<template #default="scope">
|
|
14
|
+
<p
|
|
15
|
+
:key="key"
|
|
16
|
+
v-for="{ key, value } in scope?.row?.old_values"
|
|
17
|
+
class="truncate color-9"
|
|
18
|
+
>
|
|
19
|
+
<strong>{{ $filters.ucfirst(key) }}</strong
|
|
20
|
+
><br />{{ value }}
|
|
21
|
+
</p>
|
|
22
|
+
</template>
|
|
23
|
+
</el-table-column>
|
|
24
|
+
|
|
25
|
+
<el-table-column label="New" :fit="false">
|
|
26
|
+
<template #default="scope">
|
|
27
|
+
<p
|
|
28
|
+
:key="key"
|
|
29
|
+
v-for="{ key, value } in scope?.row?.new_values"
|
|
30
|
+
class="truncate"
|
|
31
|
+
>
|
|
32
|
+
<strong>{{ $filters.ucfirst(key) }}</strong
|
|
33
|
+
><br />{{ value }}
|
|
34
|
+
</p>
|
|
35
|
+
</template>
|
|
36
|
+
</el-table-column>
|
|
37
|
+
|
|
38
|
+
<el-table-column label="User" prop="user.email" :fit="false" />
|
|
39
|
+
|
|
40
|
+
<el-table-column label="Date" :fit="false">
|
|
41
|
+
<template #default="scope">
|
|
42
|
+
{{ $filters.calendarFormat(scope?.row?.created_at) }}
|
|
43
|
+
</template>
|
|
44
|
+
</el-table-column>
|
|
45
|
+
</el-table>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script>
|
|
49
|
+
import { ElTable, ElTableColumn } from "element-plus";
|
|
50
|
+
|
|
51
|
+
export default {
|
|
52
|
+
components: {
|
|
53
|
+
ElTable,
|
|
54
|
+
ElTableColumn,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
props: ["data", "query"],
|
|
58
|
+
|
|
59
|
+
computed: {
|
|
60
|
+
audits() {
|
|
61
|
+
return (this.data || [])
|
|
62
|
+
.map((d) => {
|
|
63
|
+
d.new_values = Object.entries(d.new_values)
|
|
64
|
+
.filter(([key, value]) => value)
|
|
65
|
+
.map(([key, value]) => ({ key, value }));
|
|
66
|
+
d.old_values = Object.entries(d.old_values)
|
|
67
|
+
.filter(([key, value]) => value)
|
|
68
|
+
.map(([key, value]) => ({ key, value }));
|
|
69
|
+
return d;
|
|
70
|
+
})
|
|
71
|
+
.reverse();
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
</script>
|
|
@@ -18,7 +18,12 @@ export default [
|
|
|
18
18
|
{
|
|
19
19
|
api: {
|
|
20
20
|
params: {
|
|
21
|
-
|
|
21
|
+
index: ({ $route }) => ({
|
|
22
|
+
"filter[withTrashed]": $route.query.trashed,
|
|
23
|
+
}),
|
|
24
|
+
show: () => ({
|
|
25
|
+
include: "primary_contact",
|
|
26
|
+
}),
|
|
22
27
|
},
|
|
23
28
|
},
|
|
24
29
|
permissions: {
|
|
@@ -35,12 +40,21 @@ export default [
|
|
|
35
40
|
primary_contact: model?.primary_contact?.id || null,
|
|
36
41
|
domains: model?.domains || [],
|
|
37
42
|
seats: model?.seats != null ? model.seats : null,
|
|
38
|
-
roles:
|
|
43
|
+
roles:
|
|
44
|
+
model?.roles.map((val) => ({
|
|
45
|
+
label: val.label,
|
|
46
|
+
value: val.id,
|
|
47
|
+
})) || [],
|
|
39
48
|
sso_client_id: model?.sso_client_id || undefined,
|
|
40
49
|
sso_tenant: model?.sso_tenant || undefined,
|
|
41
50
|
sso_client_secret: model?.sso_client_secret || undefined,
|
|
42
51
|
sso_type: model?.sso_type || undefined,
|
|
43
52
|
}),
|
|
53
|
+
preparation: (props) => {
|
|
54
|
+
const data = props.form.data();
|
|
55
|
+
data.roles = data.roles.map((d) => d.value);
|
|
56
|
+
return data;
|
|
57
|
+
},
|
|
44
58
|
},
|
|
45
59
|
table: {
|
|
46
60
|
structure: [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import VelFormRole from "../../../../components/layout/FormRole.vue";
|
|
2
|
+
import VelButton from "../../../../components/basic/Button.vue";
|
|
2
3
|
import Chip from "../../../../components/layout/Chip.vue";
|
|
3
4
|
import Chips from "../../../../components/layout/Chips.vue";
|
|
4
5
|
import VelRoleLegend from "../../../../components/layout/RoleLegend.vue";
|
|
@@ -9,6 +10,7 @@ import { defaultResource, meta } from "../../../resource/index.js";
|
|
|
9
10
|
import { ElMessageBox } from "element-plus";
|
|
10
11
|
import { ElNotification } from "element-plus";
|
|
11
12
|
import { h, resolveComponent } from "vue";
|
|
13
|
+
import axios from "axios";
|
|
12
14
|
|
|
13
15
|
function generatePassword(
|
|
14
16
|
length = 20,
|
|
@@ -26,7 +28,10 @@ export default [
|
|
|
26
28
|
{
|
|
27
29
|
api: {
|
|
28
30
|
params: {
|
|
29
|
-
index: () => ({
|
|
31
|
+
index: ({ $route }) => ({
|
|
32
|
+
include: "company",
|
|
33
|
+
"filter[withTrashed]": $route.query.trashed,
|
|
34
|
+
}),
|
|
30
35
|
show: () => ({ include: "company" }),
|
|
31
36
|
},
|
|
32
37
|
},
|
|
@@ -36,11 +41,18 @@ export default [
|
|
|
36
41
|
permissions: {
|
|
37
42
|
create: ({ $store }) => $store.getters.can("write users"),
|
|
38
43
|
edit: ({ $store }) => $store.getters.can("write users"),
|
|
39
|
-
delete: ({ $store }) => $store.getters.can("delete
|
|
44
|
+
delete: ({ $store }) => $store.getters.can("delete users"),
|
|
40
45
|
},
|
|
41
46
|
form: {
|
|
42
|
-
async submit(
|
|
47
|
+
async submit(props) {
|
|
48
|
+
const { model, form, $router, $store, method, resource } =
|
|
49
|
+
props;
|
|
50
|
+
|
|
51
|
+
const hold = form.data();
|
|
52
|
+
|
|
43
53
|
try {
|
|
54
|
+
form.populate(resource.form.preparation(props));
|
|
55
|
+
|
|
44
56
|
if (method === "post") {
|
|
45
57
|
if (form.set_password) {
|
|
46
58
|
const password = generatePassword();
|
|
@@ -94,6 +106,10 @@ export default [
|
|
|
94
106
|
}
|
|
95
107
|
} catch (e) {
|
|
96
108
|
console.log(e);
|
|
109
|
+
} finally {
|
|
110
|
+
if (!form.successful || !form.__options.resetOnSuccess) {
|
|
111
|
+
form.populate(hold);
|
|
112
|
+
}
|
|
97
113
|
}
|
|
98
114
|
},
|
|
99
115
|
component,
|
|
@@ -102,9 +118,10 @@ export default [
|
|
|
102
118
|
name: model?.name ?? null,
|
|
103
119
|
email: model?.email ?? null,
|
|
104
120
|
roles: model?.overrides_roles_and_permissions
|
|
105
|
-
? model?.roles.map((val) => {
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
? model?.roles.map((val) => ({
|
|
122
|
+
label: val.label,
|
|
123
|
+
value: val.id,
|
|
124
|
+
}))
|
|
108
125
|
: [],
|
|
109
126
|
company_id: model?.company_id ?? null,
|
|
110
127
|
},
|
|
@@ -118,6 +135,11 @@ export default [
|
|
|
118
135
|
}
|
|
119
136
|
: {}),
|
|
120
137
|
}),
|
|
138
|
+
preparation: (props) => {
|
|
139
|
+
const data = props.form.data();
|
|
140
|
+
data.roles = data.roles.map((d) => d.value);
|
|
141
|
+
return data;
|
|
142
|
+
},
|
|
121
143
|
},
|
|
122
144
|
table: {
|
|
123
145
|
structure: [
|
|
@@ -132,17 +154,23 @@ export default [
|
|
|
132
154
|
key: "company",
|
|
133
155
|
sortable: true,
|
|
134
156
|
render: ({ model }) =>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
157
|
+
model.company && model.company?.deleted_at
|
|
158
|
+
? h(
|
|
159
|
+
"span",
|
|
160
|
+
{ class: "vel-basic__error" },
|
|
161
|
+
model.company.name,
|
|
162
|
+
)
|
|
163
|
+
: h(resolveComponent("router-link"), {
|
|
164
|
+
class: "underline",
|
|
165
|
+
to: {
|
|
166
|
+
name: "companies.show",
|
|
167
|
+
params: {
|
|
168
|
+
[meta(...companyResource).id]:
|
|
169
|
+
model.company_id,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
text: model.company.name,
|
|
173
|
+
}),
|
|
146
174
|
},
|
|
147
175
|
{
|
|
148
176
|
key: "role",
|
|
@@ -175,17 +203,23 @@ export default [
|
|
|
175
203
|
{
|
|
176
204
|
key: "company",
|
|
177
205
|
render: ({ model }) =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
206
|
+
model.company && model.company?.deleted_at
|
|
207
|
+
? h(
|
|
208
|
+
"span",
|
|
209
|
+
{ class: "vel-basic__error" },
|
|
210
|
+
model.company.name,
|
|
211
|
+
)
|
|
212
|
+
: h(resolveComponent("router-link"), {
|
|
213
|
+
class: "underline",
|
|
214
|
+
to: {
|
|
215
|
+
name: "companies.show",
|
|
216
|
+
params: {
|
|
217
|
+
[meta(...companyResource).id]:
|
|
218
|
+
model.company_id,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
text: model.company.name,
|
|
222
|
+
}),
|
|
189
223
|
},
|
|
190
224
|
],
|
|
191
225
|
},
|
|
@@ -199,6 +233,38 @@ export default [
|
|
|
199
233
|
],
|
|
200
234
|
},
|
|
201
235
|
show: {
|
|
236
|
+
actions: [
|
|
237
|
+
({ model, $store, $root }) =>
|
|
238
|
+
$store.getters.can("impersonate users") &&
|
|
239
|
+
h(
|
|
240
|
+
VelButton,
|
|
241
|
+
{
|
|
242
|
+
type: "danger",
|
|
243
|
+
async onClick() {
|
|
244
|
+
try {
|
|
245
|
+
const user = (
|
|
246
|
+
await axios.post(
|
|
247
|
+
`/api/users/impersonate`,
|
|
248
|
+
{
|
|
249
|
+
user_id: model.id,
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
).data.data;
|
|
253
|
+
|
|
254
|
+
$store.commit("setUser", user);
|
|
255
|
+
|
|
256
|
+
if (!$store.getters.can("view admin")) {
|
|
257
|
+
window.location = `${$root.spaUrl}?authenticated=1`;
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.log(e);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
"Impersonate",
|
|
265
|
+
),
|
|
266
|
+
...defaultResource.show.actions,
|
|
267
|
+
],
|
|
202
268
|
layout: [
|
|
203
269
|
...defaultResource.show.layout,
|
|
204
270
|
({ model }) => ({
|
|
@@ -51,17 +51,28 @@ export default {
|
|
|
51
51
|
if (this.resource.form.submit) {
|
|
52
52
|
await this.resource.form.submit(this);
|
|
53
53
|
} else {
|
|
54
|
+
const hold = this.form.data();
|
|
55
|
+
|
|
54
56
|
try {
|
|
57
|
+
this.form.populate(this.resource.form.preparation(this));
|
|
58
|
+
|
|
55
59
|
let res = await this.form.post(
|
|
56
60
|
`${this.resource.api.endpoint(this)}`,
|
|
57
61
|
);
|
|
58
62
|
|
|
59
63
|
this.$router.replace({
|
|
60
|
-
name: `${this.resource.
|
|
64
|
+
name: `${this.resource.routeName}.show`,
|
|
61
65
|
params: { [this.resource.id]: res.data.id },
|
|
62
66
|
});
|
|
63
67
|
} catch (e) {
|
|
64
68
|
console.log(e);
|
|
69
|
+
} finally {
|
|
70
|
+
if (
|
|
71
|
+
!this.form.successful ||
|
|
72
|
+
!this.form.__options.resetOnSuccess
|
|
73
|
+
) {
|
|
74
|
+
this.form.populate(hold);
|
|
75
|
+
}
|
|
65
76
|
}
|
|
66
77
|
}
|
|
67
78
|
},
|
|
@@ -43,9 +43,12 @@ export default {
|
|
|
43
43
|
},
|
|
44
44
|
|
|
45
45
|
beforeMount() {
|
|
46
|
-
this.form = new Form(
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
this.form = new Form(
|
|
47
|
+
{ _method: "PATCH", ...this.resource.form.fields(this) },
|
|
48
|
+
{
|
|
49
|
+
resetOnSuccess: false,
|
|
50
|
+
},
|
|
51
|
+
);
|
|
49
52
|
},
|
|
50
53
|
|
|
51
54
|
async mounted() {
|
|
@@ -73,17 +76,28 @@ export default {
|
|
|
73
76
|
if (this.resource.form.submit) {
|
|
74
77
|
await this.resource.form.submit(this);
|
|
75
78
|
} else {
|
|
79
|
+
const hold = this.form.data();
|
|
80
|
+
|
|
76
81
|
try {
|
|
77
|
-
|
|
82
|
+
this.form.populate(this.resource.form.preparation(this));
|
|
83
|
+
|
|
84
|
+
let res = await this.form.post(
|
|
78
85
|
`${this.resource.api.endpoint(this)}/${this.model.id}`,
|
|
79
86
|
);
|
|
80
87
|
|
|
81
88
|
this.$router.replace({
|
|
82
|
-
name: `${this.resource.
|
|
89
|
+
name: `${this.resource.routeName}.show`,
|
|
83
90
|
params: { [this.resource.id]: res.data.id },
|
|
84
91
|
});
|
|
85
92
|
} catch (e) {
|
|
86
93
|
console.log(e);
|
|
94
|
+
} finally {
|
|
95
|
+
if (
|
|
96
|
+
!this.form.successful ||
|
|
97
|
+
!this.form.__options.resetOnSuccess
|
|
98
|
+
) {
|
|
99
|
+
this.form.populate(hold);
|
|
100
|
+
}
|
|
87
101
|
}
|
|
88
102
|
}
|
|
89
103
|
},
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<form @submit.prevent="submit">
|
|
4
4
|
<template v-for="(item, index) in resource.form.structure" :key="index">
|
|
5
5
|
<component
|
|
6
|
+
v-if="!item.condition || item.condition(this)"
|
|
6
7
|
:is="item.render ? item.render(this) : 'VelBasic'"
|
|
7
8
|
v-model="form[item.key]"
|
|
8
9
|
:type="item.type || 'text'"
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
:name="item.key"
|
|
11
12
|
:placeholder="
|
|
12
13
|
item.placeholder ||
|
|
14
|
+
item.label ||
|
|
13
15
|
item.key[0].toUpperCase() + item.key.slice(1)
|
|
14
16
|
"
|
|
15
17
|
:label="
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
<div class="bg-0 p-3 box-shadow-1 border-r-4 mb-6">
|
|
12
12
|
<VelPageHeader
|
|
13
13
|
:icon="resource.icon"
|
|
14
|
-
:title="
|
|
14
|
+
:title="
|
|
15
|
+
resource.modelTitle
|
|
16
|
+
? resource.modelTitle(this)
|
|
17
|
+
: `${model.name ?? model.id} ${model.last_name ?? ''}`
|
|
18
|
+
"
|
|
15
19
|
>
|
|
16
20
|
<template
|
|
17
21
|
v-for="(rendered, index) in renderedActions"
|
|
@@ -99,7 +103,10 @@ export default {
|
|
|
99
103
|
computed: {
|
|
100
104
|
// This boolean helps determine if we are the final depth of route being rendered
|
|
101
105
|
deepestRoute() {
|
|
102
|
-
return
|
|
106
|
+
return (
|
|
107
|
+
this.depth ===
|
|
108
|
+
this.$route.matched.filter((d) => d.components).length
|
|
109
|
+
);
|
|
103
110
|
},
|
|
104
111
|
|
|
105
112
|
// Compute rendered layout once
|
|
@@ -120,7 +127,7 @@ export default {
|
|
|
120
127
|
|
|
121
128
|
axios
|
|
122
129
|
.get(
|
|
123
|
-
`${this.resource.api.endpoint(this)}/${this.$route.params[`${this.resource.
|
|
130
|
+
`${this.resource.api.endpoint(this)}/${this.$route.params[`${this.resource.id}`]}`,
|
|
124
131
|
{
|
|
125
132
|
params: this.resource.api.params.show(this),
|
|
126
133
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { merge, kebabCase, cloneDeepWith } from "lodash";
|
|
3
|
+
import { merge, kebabCase, snakeCase, cloneDeepWith } from "lodash";
|
|
4
4
|
import axios from "axios";
|
|
5
5
|
import { h, resolveComponent } from "vue";
|
|
6
6
|
|
|
@@ -30,7 +30,9 @@ export function meta(name = "default", properties = {}) {
|
|
|
30
30
|
label: singular,
|
|
31
31
|
multiLabel: name,
|
|
32
32
|
slug,
|
|
33
|
-
|
|
33
|
+
path: properties.path || `/${slug}`,
|
|
34
|
+
routeName: properties.routeName || slug,
|
|
35
|
+
id: properties.id || `${snakeCase(slug)}Id`,
|
|
34
36
|
icon: `icon-${singular}`,
|
|
35
37
|
api: {
|
|
36
38
|
endpoint: () => `/api/${properties.slug || kebabCase(name)}`,
|
|
@@ -51,11 +53,13 @@ export function meta(name = "default", properties = {}) {
|
|
|
51
53
|
form: {
|
|
52
54
|
component: null,
|
|
53
55
|
fields: () => ({}),
|
|
56
|
+
preparation: ({ form }) => form.data(),
|
|
54
57
|
structure: [],
|
|
55
58
|
},
|
|
56
59
|
table: {
|
|
57
60
|
actions: [
|
|
58
61
|
({ model, resource }, { $router }) =>
|
|
62
|
+
!model.deleted_at &&
|
|
59
63
|
h(
|
|
60
64
|
VelButton,
|
|
61
65
|
{
|
|
@@ -64,7 +68,7 @@ export function meta(name = "default", properties = {}) {
|
|
|
64
68
|
type: "primary",
|
|
65
69
|
onClick: () => {
|
|
66
70
|
$router.push({
|
|
67
|
-
name: `${resource.
|
|
71
|
+
name: `${resource.routeName}.show`,
|
|
68
72
|
params: {
|
|
69
73
|
[resource.id]: model.id,
|
|
70
74
|
},
|
|
@@ -77,21 +81,24 @@ export function meta(name = "default", properties = {}) {
|
|
|
77
81
|
const { $router } = props;
|
|
78
82
|
|
|
79
83
|
if (resource.permissions.edit(props, { model })) {
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
return (
|
|
85
|
+
!model.deleted_at &&
|
|
86
|
+
h(
|
|
87
|
+
VelButton,
|
|
88
|
+
{
|
|
89
|
+
tag: "a",
|
|
90
|
+
size: "small",
|
|
91
|
+
onClick: () => {
|
|
92
|
+
$router.push({
|
|
93
|
+
name: `${resource.routeName}.edit`,
|
|
94
|
+
params: {
|
|
95
|
+
[resource.id]: model.id,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
},
|
|
92
99
|
},
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
() => "Edit",
|
|
101
|
+
)
|
|
95
102
|
);
|
|
96
103
|
}
|
|
97
104
|
},
|
|
@@ -99,52 +106,56 @@ export function meta(name = "default", properties = {}) {
|
|
|
99
106
|
const { $emit } = props;
|
|
100
107
|
|
|
101
108
|
if (resource.permissions.delete(props, { model })) {
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
return (
|
|
110
|
+
!model.deleted_at &&
|
|
111
|
+
h({
|
|
112
|
+
data: () => ({
|
|
113
|
+
loading: false,
|
|
114
|
+
}),
|
|
115
|
+
render() {
|
|
116
|
+
return h(
|
|
117
|
+
ElPopconfirm,
|
|
118
|
+
{
|
|
119
|
+
title: `Are you sure you want to delete this ${resource.singular}?`,
|
|
120
|
+
confirmButtonText: "Delete",
|
|
121
|
+
cancelButtonText: "Cancel",
|
|
122
|
+
confirmButtonType: "danger",
|
|
123
|
+
onConfirm: async () => {
|
|
124
|
+
this.loading = true;
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
await axios.delete(
|
|
127
|
+
`${resource.api.endpoint(props)}/${model.id}`,
|
|
128
|
+
);
|
|
120
129
|
|
|
121
|
-
|
|
130
|
+
$emit("reload");
|
|
122
131
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
ElNotification({
|
|
133
|
+
title: "Success",
|
|
134
|
+
message: `${resource.singularTitle} with id ${model.id} deleted.`,
|
|
135
|
+
type: "success",
|
|
136
|
+
});
|
|
128
137
|
|
|
129
|
-
|
|
138
|
+
this.loading = false;
|
|
139
|
+
},
|
|
130
140
|
},
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
{
|
|
142
|
+
reference: () =>
|
|
143
|
+
h(
|
|
144
|
+
VelButton,
|
|
145
|
+
{
|
|
146
|
+
tag: "a",
|
|
147
|
+
type: "danger",
|
|
148
|
+
size: "small",
|
|
149
|
+
loading:
|
|
150
|
+
this.loading,
|
|
151
|
+
},
|
|
152
|
+
() => `Delete`,
|
|
153
|
+
),
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
);
|
|
148
159
|
}
|
|
149
160
|
},
|
|
150
161
|
],
|
|
@@ -216,7 +227,7 @@ export function meta(name = "default", properties = {}) {
|
|
|
216
227
|
size: "large",
|
|
217
228
|
onClick: () => {
|
|
218
229
|
$router.push({
|
|
219
|
-
name: `${resource.
|
|
230
|
+
name: `${resource.routeName}.create`,
|
|
220
231
|
});
|
|
221
232
|
},
|
|
222
233
|
},
|
|
@@ -257,7 +268,7 @@ export function meta(name = "default", properties = {}) {
|
|
|
257
268
|
type: "primary",
|
|
258
269
|
onClick: () => {
|
|
259
270
|
$router.push({
|
|
260
|
-
name: `${resource.
|
|
271
|
+
name: `${resource.routeName}.edit`,
|
|
261
272
|
params: {
|
|
262
273
|
[resource.id]: model.id,
|
|
263
274
|
},
|
|
@@ -293,7 +304,7 @@ export function meta(name = "default", properties = {}) {
|
|
|
293
304
|
);
|
|
294
305
|
|
|
295
306
|
$router.push({
|
|
296
|
-
name: `${resource.
|
|
307
|
+
name: `${resource.routeName}.index`,
|
|
297
308
|
});
|
|
298
309
|
},
|
|
299
310
|
},
|
|
@@ -335,6 +346,8 @@ export function meta(name = "default", properties = {}) {
|
|
|
335
346
|
() =>
|
|
336
347
|
resource.description.structure.map(
|
|
337
348
|
(item, index) =>
|
|
349
|
+
(!item.condition ||
|
|
350
|
+
item.condition(props)) &&
|
|
338
351
|
h(
|
|
339
352
|
ElDescriptionsItem,
|
|
340
353
|
{
|
|
@@ -397,6 +410,15 @@ export function columns(columns = []) {
|
|
|
397
410
|
: (props.model?.[column.key] ?? null);
|
|
398
411
|
return fields;
|
|
399
412
|
}, {}),
|
|
413
|
+
preparation: (props) =>
|
|
414
|
+
columns
|
|
415
|
+
.filter((column) => !column.filter?.form)
|
|
416
|
+
.reduce((fields, column) => {
|
|
417
|
+
fields[column.key] = column.preparation
|
|
418
|
+
? column.preparation(props)
|
|
419
|
+
: props.form[column.key];
|
|
420
|
+
return fields;
|
|
421
|
+
}, {}),
|
|
400
422
|
structure: columns
|
|
401
423
|
.filter((column) => !column.filter?.form)
|
|
402
424
|
.map((column) => ({
|
|
@@ -408,25 +430,20 @@ export function columns(columns = []) {
|
|
|
408
430
|
}
|
|
409
431
|
|
|
410
432
|
// Export resource
|
|
411
|
-
export function routes(
|
|
412
|
-
node,
|
|
413
|
-
name,
|
|
414
|
-
properties = {},
|
|
415
|
-
children = [],
|
|
416
|
-
isChild = false,
|
|
417
|
-
) {
|
|
433
|
+
export function routes(node, name, properties = {}, children = []) {
|
|
418
434
|
const resource = meta(name, properties);
|
|
419
435
|
|
|
420
436
|
return [
|
|
421
437
|
{
|
|
422
|
-
path:
|
|
438
|
+
path: resource.path,
|
|
423
439
|
component: node ? "" : require("../resource/parent.vue").default,
|
|
424
|
-
name
|
|
440
|
+
name: `${resource.routeName}`,
|
|
425
441
|
meta: {
|
|
426
442
|
resource,
|
|
427
443
|
title: resource.title,
|
|
428
444
|
icon: resource.icon,
|
|
429
445
|
breadcrumb: () => resource.title,
|
|
446
|
+
...properties.meta,
|
|
430
447
|
},
|
|
431
448
|
children: [
|
|
432
449
|
{
|
|
@@ -434,21 +451,21 @@ export function routes(
|
|
|
434
451
|
component: node
|
|
435
452
|
? ""
|
|
436
453
|
: require("../resource/Children/index.vue").default,
|
|
437
|
-
name: `${resource.
|
|
454
|
+
name: `${resource.routeName}.index`,
|
|
438
455
|
},
|
|
439
456
|
{
|
|
440
457
|
path: "create",
|
|
441
458
|
component: node
|
|
442
459
|
? ""
|
|
443
460
|
: require("../resource/Children/create.vue").default,
|
|
444
|
-
name: `${resource.
|
|
461
|
+
name: `${resource.routeName}.create`,
|
|
445
462
|
},
|
|
446
463
|
{
|
|
447
464
|
path: `:${resource.id}`,
|
|
448
465
|
component: node
|
|
449
466
|
? ""
|
|
450
467
|
: require("../resource/Children/show.vue").default,
|
|
451
|
-
name: `${resource.
|
|
468
|
+
name: `${resource.routeName}.show`,
|
|
452
469
|
// Remove leading / for nested routes or they'll resolve to the root of the site
|
|
453
470
|
children: cloneDeepWith(children, (value, key) => {
|
|
454
471
|
if (
|
|
@@ -468,7 +485,7 @@ export function routes(
|
|
|
468
485
|
component: node
|
|
469
486
|
? ""
|
|
470
487
|
: require("../resource/Children/edit.vue").default,
|
|
471
|
-
name: `${resource.
|
|
488
|
+
name: `${resource.routeName}.edit`,
|
|
472
489
|
meta: {
|
|
473
490
|
breadcrumb: ({ $route }) => $route.params[resource.id],
|
|
474
491
|
},
|
package/index.js
CHANGED
|
@@ -38,6 +38,7 @@ export { default as Navigation } from "./_Build/vue/components/layout/Navigation
|
|
|
38
38
|
export { default as PageTitle } from "./_Build/vue/components/layout/pageTitle.vue";
|
|
39
39
|
export { default as Alert } from "./_Build/vue/components/layout/Alert.vue";
|
|
40
40
|
export { default as Tooltip } from "./_Build/vue/components/layout/Tooltip.vue";
|
|
41
|
+
export { default as Audit } from "./_Build/vue/components/layout/Audit.vue";
|
|
41
42
|
export { default as Menu } from "./_Build/vue/components/navigation/Menu.vue";
|
|
42
43
|
export { default as MenuItem } from "./_Build/vue/components/navigation/MenuItem.vue";
|
|
43
44
|
export { default as MenuItemGroup } from "./_Build/vue/components/navigation/MenuItemGroup.vue";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fishawack/lab-velocity",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.32",
|
|
4
4
|
"description": "Avalere Health branded style system",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"setup": "npm ci || npm i && npm run content",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@tiptap/starter-kit": "^2.11.2",
|
|
55
55
|
"@tiptap/vue-3": "^2.11.2",
|
|
56
56
|
"axios": "^1.11.0",
|
|
57
|
-
"element-plus": "^2.
|
|
57
|
+
"element-plus": "^2.11.8",
|
|
58
58
|
"form-backend-validation": "github:mikemellor11/form-backend-validation#master",
|
|
59
59
|
"lodash": "^4.17.21",
|
|
60
60
|
"quill": "^1.3.7",
|