@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.
@@ -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
- @change="handleInput"
18
- @clear="this.$emit('clear')"
19
- @blur="this.$emit('blur')"
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
- <el-option
25
- v-if="!options[0]?.label"
26
- v-for="(label, value) in options"
27
- :key="value"
28
- :label="label"
29
- :value="castValue(value)"
30
- >
31
- </el-option>
32
- <el-option
33
- v-else
34
- v-for="option in options"
35
- :key="option.value"
36
- :label="option.label"
37
- :disabled="option.disabled"
38
- :value="castValue(option.value)"
39
- >
40
- </el-option>
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
- components: {
89
- XInput,
90
- ElOption,
91
- ElSelect,
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
- show: () => ({ include: "primary_contact" }),
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: model?.roles.map((val) => val.id) || [],
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: () => ({ include: "company" }),
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 companies"),
44
+ delete: ({ $store }) => $store.getters.can("delete users"),
40
45
  },
41
46
  form: {
42
- async submit({ model, form, $router, $store, method, resource }) {
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
- return val.id;
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
- h(resolveComponent("router-link"), {
136
- class: "underline",
137
- to: {
138
- name: "companies.show",
139
- params: {
140
- [meta(...companyResource).id]:
141
- model.company_id,
142
- },
143
- },
144
- text: model.company?.name,
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
- h(resolveComponent("router-link"), {
179
- class: "underline",
180
- to: {
181
- name: "companies.show",
182
- params: {
183
- [meta(...companyResource).id]:
184
- model.company_id,
185
- },
186
- },
187
- text: model.company.name,
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.slug}.show`,
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(this.resource.form.fields(this), {
47
- resetOnSuccess: false,
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
- let res = await this.form.patch(
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.slug}.show`,
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="`${model.name ?? model.id} ${model.last_name ?? ''}`"
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 this.depth === this.$route.matched.length;
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.slug}Id`]}`,
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
- id: properties.id || `${slug}Id`,
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.slug}.show`,
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 h(
81
- VelButton,
82
- {
83
- tag: "a",
84
- size: "small",
85
- onClick: () => {
86
- $router.push({
87
- name: `${resource.slug}.edit`,
88
- params: {
89
- [resource.id]: model.id,
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
- () => "Edit",
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 h({
103
- data: () => ({
104
- loading: false,
105
- }),
106
- render() {
107
- return h(
108
- ElPopconfirm,
109
- {
110
- title: `Are you sure you want to delete this ${resource.singular}?`,
111
- confirmButtonText: "Delete",
112
- cancelButtonText: "Cancel",
113
- confirmButtonType: "danger",
114
- onConfirm: async () => {
115
- this.loading = true;
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
- await axios.delete(
118
- `${resource.api.endpoint(props)}/${model.id}`,
119
- );
126
+ await axios.delete(
127
+ `${resource.api.endpoint(props)}/${model.id}`,
128
+ );
120
129
 
121
- $emit("reload");
130
+ $emit("reload");
122
131
 
123
- ElNotification({
124
- title: "Success",
125
- message: `${resource.singularTitle} with id ${model.id} deleted.`,
126
- type: "success",
127
- });
132
+ ElNotification({
133
+ title: "Success",
134
+ message: `${resource.singularTitle} with id ${model.id} deleted.`,
135
+ type: "success",
136
+ });
128
137
 
129
- this.loading = false;
138
+ this.loading = false;
139
+ },
130
140
  },
131
- },
132
- {
133
- reference: () =>
134
- h(
135
- VelButton,
136
- {
137
- tag: "a",
138
- type: "danger",
139
- size: "small",
140
- loading: this.loading,
141
- },
142
- () => `Delete`,
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.slug}.create`,
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.slug}.edit`,
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.slug}.index`,
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: `${isChild ? "" : "/"}${resource.slug}`,
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.slug}.index`,
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.slug}.create`,
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.slug}.show`,
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.slug}.edit`,
488
+ name: `${resource.routeName}.edit`,
472
489
  meta: {
473
490
  breadcrumb: ({ $route }) => $route.params[resource.id],
474
491
  },
@@ -1,4 +1,5 @@
1
1
  @import "element-plus/theme-chalk/el-date-picker";
2
+ @import "element-plus/theme-chalk/el-date-picker-panel";
2
3
 
3
4
  .vel-datepicker {
4
5
  // --el-input-height:38px;
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.30",
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.7.8",
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",