@fishawack/lab-velocity 2.0.0-beta.40 → 2.0.0-beta.42

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.
Files changed (30) hide show
  1. package/README.md +4 -2
  2. package/_Build/js/libs/build-id.js +14 -0
  3. package/_Build/js/libs/filters.js +36 -0
  4. package/_Build/js/libs/globals.js +7 -0
  5. package/_Build/js/libs/router.js +22 -0
  6. package/_Build/js/libs/routes.js +29 -0
  7. package/_Build/js/libs/store.js +21 -0
  8. package/_Build/js/libs/utility.js +161 -0
  9. package/_Build/vue/components/form/Avatar.vue +86 -0
  10. package/_Build/vue/components/layout/Audit.vue +124 -56
  11. package/_Build/vue/components/layout/Layout.vue +19 -1
  12. package/_Build/vue/components/layout/TableSorter.vue +42 -12
  13. package/_Build/vue/modules/AuthModule/js/router.js +20 -36
  14. package/_Build/vue/modules/AuthModule/routes/PCompanies/columns.js +268 -0
  15. package/_Build/vue/modules/AuthModule/routes/PCompanies/resource.js +173 -232
  16. package/_Build/vue/modules/AuthModule/routes/PIntegrations/resource.js +122 -0
  17. package/_Build/vue/modules/AuthModule/routes/PTeams/resource.js +1 -0
  18. package/_Build/vue/modules/AuthModule/routes/PUsers/columns.js +349 -0
  19. package/_Build/vue/modules/AuthModule/routes/PUsers/resource.js +142 -262
  20. package/_Build/vue/modules/resource/Children/create.vue +2 -2
  21. package/_Build/vue/modules/resource/Children/edit.vue +2 -2
  22. package/_Build/vue/modules/resource/Children/partials/form.vue +71 -21
  23. package/_Build/vue/modules/resource/Children/show.vue +24 -2
  24. package/_Build/vue/modules/resource/index.js +12 -4
  25. package/components/_form.scss +18 -0
  26. package/components/_menu.scss +0 -5
  27. package/index.js +16 -0
  28. package/package.json +3 -1
  29. package/_Build/vue/modules/AuthModule/routes/PCompanies/form.vue +0 -205
  30. package/_Build/vue/modules/AuthModule/routes/PUsers/form.vue +0 -193
@@ -1,16 +1,14 @@
1
+ import { merge } from "lodash";
2
+ import { h } from "vue";
3
+ import axios from "axios";
4
+ import { ElMessageBox, ElNotification } from "element-plus";
5
+
6
+ import { columns, defaultResource } from "../../../resource/index.js";
7
+ import usersColumns, { detectedCompany } from "./columns.js";
8
+
1
9
  import VelFormRole from "../../../../components/layout/FormRole.vue";
2
10
  import VelButton from "../../../../components/basic/Button.vue";
3
- import Chip from "../../../../components/layout/Chip.vue";
4
- import Chips from "../../../../components/layout/Chips.vue";
5
11
  import VelRoleLegend from "../../../../components/layout/RoleLegend.vue";
6
- import component from "./form.vue";
7
- import companyResource from "../PCompanies/resource.js";
8
- import { defaultResource, meta } from "../../../resource/index.js";
9
-
10
- import { ElMessageBox } from "element-plus";
11
- import { ElNotification } from "element-plus";
12
- import { h, resolveComponent } from "vue";
13
- import axios from "axios";
14
12
 
15
13
  function generatePassword(
16
14
  length = 20,
@@ -38,285 +36,167 @@ export default [
38
36
  searchable: {
39
37
  value: "email",
40
38
  },
39
+ auditable: true,
41
40
  permissions: {
42
41
  create: ({ $store }) => $store.getters.can("write users"),
43
42
  edit: ({ $store }) => $store.getters.can("write users"),
44
43
  delete: ({ $store }) => $store.getters.can("delete users"),
45
44
  },
46
- form: {
47
- async submit(props) {
48
- const { model, form, $router, $store, method, resource } =
49
- props;
45
+ ...merge(columns(usersColumns), {
46
+ form: {
47
+ fields: ({ model, method }) => ({
48
+ ...columns(usersColumns).form.fields({ model }),
49
+ ...(method === "post"
50
+ ? {
51
+ notify_user: true,
52
+ force_password_change: true,
53
+ set_password: true,
54
+ password: null,
55
+ password_confirmation: null,
56
+ }
57
+ : {}),
58
+ }),
59
+ async submit(props) {
60
+ const { model, form, $router, $store, method, resource } =
61
+ props;
50
62
 
51
- const hold = form.data();
63
+ const hold = form.data();
52
64
 
53
- try {
54
- form.populate(resource.form.preparation(props));
65
+ try {
66
+ form.populate(resource.form.preparation(props));
55
67
 
56
- if (method === "post") {
57
- const isSSOCompany = props.$refs?.form?.isSSOCompany;
68
+ if (method === "post") {
69
+ const isSSOCompany =
70
+ !!detectedCompany.value?.sso_enabled;
58
71
 
59
- if (!isSSOCompany && form.set_password) {
60
- const password = generatePassword();
61
- form.password = password;
62
- form.password_confirmation = password;
63
- }
72
+ if (!isSSOCompany && form.set_password) {
73
+ const password = generatePassword();
74
+ form.password = password;
75
+ form.password_confirmation = password;
76
+ }
64
77
 
65
- let res = await form.post(`/api/users`);
78
+ let res = await form.post(`/api/users`);
66
79
 
67
- if (
68
- !isSSOCompany &&
69
- form.set_password &&
70
- !form.notify_user
71
- ) {
72
- ElMessageBox.alert(
73
- `<p>The password below will not be shown again. Ensure you've taken a copy if you plan to send manually. <br><br><strong>Email</strong>: ${form.email}<br> <strong>Password</strong>: ${form.password}</p>`,
74
- "User Created",
75
- {
76
- confirmButtonText: "Ok",
77
- dangerouslyUseHTMLString: true,
78
- },
79
- )
80
- .then(() => {
81
- $router.replace({
82
- name: "users.show",
83
- params: { [resource.id]: res.data.id },
84
- });
85
- })
86
- .catch(() => {});
80
+ if (
81
+ !isSSOCompany &&
82
+ form.set_password &&
83
+ !form.notify_user
84
+ ) {
85
+ ElMessageBox.alert(
86
+ `<p>The password below will not be shown again. Ensure you've taken a copy if you plan to send manually. <br><br><strong>Email</strong>: ${form.email}<br> <strong>Password</strong>: ${form.password}</p>`,
87
+ "User Created",
88
+ {
89
+ confirmButtonText: "Ok",
90
+ dangerouslyUseHTMLString: true,
91
+ },
92
+ )
93
+ .then(() => {
94
+ $router.replace({
95
+ name: "users.show",
96
+ params: {
97
+ [resource.id]: res.data.id,
98
+ },
99
+ });
100
+ })
101
+ .catch(() => {});
102
+ } else {
103
+ ElNotification({
104
+ title: "Success",
105
+ message: isSSOCompany
106
+ ? "SSO user pre-provisioned successfully"
107
+ : "User created a notified of their new account",
108
+ type: "success",
109
+ });
110
+
111
+ $router.replace({
112
+ name: "users.show",
113
+ params: {
114
+ [resource.id]: res.data.id,
115
+ },
116
+ });
117
+ }
87
118
  } else {
88
- ElNotification({
89
- title: "Success",
90
- message: isSSOCompany
91
- ? "SSO user pre-provisioned successfully"
92
- : "User created a notified of their new account",
93
- type: "success",
94
- });
119
+ let res = await form.post(`/api/users/${model.id}`);
120
+
121
+ if (res.data.id === $store.state.auth.user.id) {
122
+ await $store.dispatch("getUser");
123
+ }
95
124
 
96
125
  $router.replace({
97
126
  name: "users.show",
98
- params: { [resource.id]: res.data.id },
127
+ params: {
128
+ [resource.id]: res.data.id,
129
+ },
99
130
  });
100
131
  }
101
- } else {
102
- let res = await form.patch(`/api/users/${model.id}`);
103
-
104
- // if changing ourselves, re-fetch user data
105
- if (res.data.id === $store.state.auth.user.id) {
106
- await $store.dispatch("getUser");
132
+ } catch (e) {
133
+ console.log(e);
134
+ } finally {
135
+ if (
136
+ !form.successful ||
137
+ !form.__options.resetOnSuccess
138
+ ) {
139
+ form.populate(hold);
107
140
  }
108
-
109
- $router.replace({
110
- name: "users.show",
111
- params: { [resource.id]: res.data.id },
112
- });
113
141
  }
114
- } catch (e) {
115
- console.log(e);
116
- } finally {
117
- if (!form.successful || !form.__options.resetOnSuccess) {
118
- form.populate(hold);
119
- }
120
- }
121
- },
122
- component,
123
- fields: ({ model, method }) => ({
124
- ...{
125
- name: model?.name ?? null,
126
- email: model?.email ?? null,
127
- roles: model?.overrides_roles_and_permissions
128
- ? model?.roles.map((val) => ({
129
- label: val.label,
130
- value: val.id,
131
- }))
132
- : [],
133
- company_id: model?.company_id ?? null,
134
142
  },
135
- ...(method === "post"
136
- ? {
137
- notify_user: true,
138
- force_password_change: true,
139
- set_password: true,
140
- password: null,
141
- password_confirmation: null,
142
- }
143
- : {}),
144
- }),
145
- preparation: (props) => {
146
- const data = props.form.data();
147
- data.roles = data.roles.map((d) => d.value);
148
- return data;
149
143
  },
150
- },
151
- table: {
152
- structure: () => [
153
- {
154
- key: "name",
155
- sortable: true,
156
- },
157
- {
158
- key: "email",
159
- },
160
- {
161
- key: "company",
162
- sortable: true,
163
- render: ({ model }) =>
164
- model.company && model.company?.deleted_at
165
- ? h(
166
- "span",
167
- { class: "vel-basic__error" },
168
- model.company.name,
169
- )
170
- : h(resolveComponent("router-link"), {
171
- class: "underline",
172
- to: {
173
- name: "companies.show",
174
- params: {
175
- [meta(...companyResource).id]:
176
- model.company_id,
177
- },
178
- },
179
- text: model.company.name,
180
- }),
181
- },
182
- {
183
- key: "role",
184
- render: ({ model }) =>
185
- h(
186
- !model.overrides_roles_and_permissions ||
187
- model.roles.length === 1
188
- ? Chip
189
- : Chips,
190
- !model.overrides_roles_and_permissions
191
- ? {
192
- name: "inherited",
193
- label: "Inherited",
194
- }
195
- : model.roles.length === 1
196
- ? {
197
- name: model.roles[0].name,
198
- label: model.roles[0].label,
199
- }
200
- : { array: model.roles },
201
- ),
202
- },
203
- {
204
- key: "verified",
205
- label: "Verified",
206
- render: ({ model }) =>
144
+ index: {
145
+ layout: [
146
+ ...defaultResource.index.layout,
147
+ () =>
148
+ h(VelRoleLegend, {
149
+ class: "mt-5",
150
+ }),
151
+ ],
152
+ },
153
+ show: {
154
+ actions: [
155
+ ({ model, $store, $root }) =>
156
+ $store.getters.can("impersonate users") &&
207
157
  h(
208
- "span",
158
+ VelButton,
209
159
  {
210
- title: model.email_verified_at
211
- ? `Verified: ${new Date(model.email_verified_at).toLocaleString()}`
212
- : "Not verified",
213
- style: {
214
- color: model.email_verified_at
215
- ? "green"
216
- : "red",
217
- fontSize: "18px",
218
- },
219
- },
220
- model.email_verified_at ? "✓" : "✗",
221
- ),
222
- },
223
- ],
224
- },
225
- description: {
226
- structure: () => [
227
- {
228
- key: "email",
229
- },
230
- {
231
- key: "company",
232
- render: ({ model }) =>
233
- model.company && model.company?.deleted_at
234
- ? h(
235
- "span",
236
- { class: "vel-basic__error" },
237
- model.company.name,
238
- )
239
- : h(resolveComponent("router-link"), {
240
- class: "underline",
241
- to: {
242
- name: "companies.show",
243
- params: {
244
- [meta(...companyResource).id]:
245
- model.company_id,
246
- },
247
- },
248
- text: model.company.name,
249
- }),
250
- },
251
- {
252
- key: "email_verified_at",
253
- label: "Email Verified",
254
- render: ({ model }) =>
255
- h(
256
- "span",
257
- {},
258
- model.email_verified_at
259
- ? new Date(
260
- model.email_verified_at,
261
- ).toLocaleString()
262
- : "Not verified",
263
- ),
264
- },
265
- ],
266
- },
267
- index: {
268
- layout: [
269
- ...defaultResource.index.layout,
270
- () =>
271
- h(VelRoleLegend, {
272
- class: "mt-5",
273
- }),
274
- ],
275
- },
276
- show: {
277
- actions: [
278
- ({ model, $store, $root }) =>
279
- $store.getters.can("impersonate users") &&
280
- h(
281
- VelButton,
282
- {
283
- type: "danger",
284
- async onClick() {
285
- try {
286
- const user = (
287
- await axios.post(
288
- `/api/users/impersonate`,
289
- {
290
- user_id: model.id,
291
- },
292
- )
293
- ).data.data;
160
+ type: "danger",
161
+ async onClick() {
162
+ try {
163
+ const user = (
164
+ await axios.post(
165
+ `/api/users/impersonate`,
166
+ {
167
+ user_id: model.id,
168
+ },
169
+ )
170
+ ).data.data;
294
171
 
295
- $store.commit("setUser", user);
172
+ $store.commit("setUser", user);
296
173
 
297
- if (!$store.getters.can("view admin")) {
298
- window.location = `${$root.spaUrl}?authenticated=1`;
174
+ if (!$store.getters.can("view admin")) {
175
+ window.location = `${$root.spaUrl}?authenticated=1`;
176
+ }
177
+ } catch (e) {
178
+ console.log(e);
299
179
  }
300
- } catch (e) {
301
- console.log(e);
302
- }
180
+ },
181
+ },
182
+ "Impersonate",
183
+ ),
184
+ ...defaultResource.show.actions,
185
+ ],
186
+ tabs: [
187
+ ...defaultResource.show.tabs,
188
+ ({ model }) => ({
189
+ label: "Access control",
190
+ component: h(VelFormRole, {
191
+ overrides: model.overrides_roles_and_permissions,
192
+ form: {
193
+ roles: model.roles.map((d) => d.id),
303
194
  },
304
- },
305
- "Impersonate",
306
- ),
307
- ...defaultResource.show.actions,
308
- ],
309
- tabs: [
310
- ...defaultResource.show.tabs,
311
- ({ model }) => ({
312
- label: "Access control",
313
- component: h(VelFormRole, {
314
- overrides: model.overrides_roles_and_permissions,
315
- form: { roles: model.roles.map((d) => d.id) },
316
- readonly: true,
195
+ readonly: true,
196
+ }),
317
197
  }),
318
- }),
319
- ],
320
- },
198
+ ],
199
+ },
200
+ }),
321
201
  },
322
202
  ];
@@ -4,7 +4,7 @@
4
4
  <div class="grid__1/1 mb-4">
5
5
  <h2 class="h1">Create {{ resource.singular }}</h2>
6
6
  </div>
7
- <div class="mt grid__1/2">
7
+ <div :class="['mt', resource.form.class]">
8
8
  <component
9
9
  :is="resource.form.component ?? 'XForm'"
10
10
  ref="form"
@@ -51,7 +51,7 @@ export default {
51
51
  if (this.resource.form.submit) {
52
52
  await this.resource.form.submit(this);
53
53
  } else {
54
- const hold = JSON.parse(JSON.stringify(this.form.data()));
54
+ const hold = { ...this.form.data() };
55
55
 
56
56
  try {
57
57
  this.form.populate(this.resource.form.preparation(this));
@@ -4,7 +4,7 @@
4
4
  <div class="grid__1/1 mb-4">
5
5
  <h2 class="h1">Edit {{ resource.singular }}</h2>
6
6
  </div>
7
- <div class="grid__1/2">
7
+ <div :class="resource.form.class">
8
8
  <component
9
9
  :is="resource.form.component ?? 'XForm'"
10
10
  ref="form"
@@ -76,7 +76,7 @@ export default {
76
76
  if (this.resource.form.submit) {
77
77
  await this.resource.form.submit(this);
78
78
  } else {
79
- const hold = JSON.parse(JSON.stringify(this.form.data()));
79
+ const hold = { ...this.form.data() };
80
80
 
81
81
  try {
82
82
  this.form.populate(this.resource.form.preparation(this));
@@ -1,27 +1,37 @@
1
1
  <!-- eslint-disable vue/no-mutating-props -->
2
2
  <template>
3
- <form @submit.prevent="submit">
4
- <template
5
- v-for="(item, index) in resource.form.structure(this)"
6
- :key="index"
3
+ <form @submit.prevent="submit" class="vel-resource-form">
4
+ <component
5
+ :is="segment.group ? 'fieldset' : 'div'"
6
+ v-for="(segment, sIndex) in segments"
7
+ :key="sIndex"
8
+ :class="{ 'vel-form-group': segment.group }"
7
9
  >
8
- <component
9
- :is="item.render ? item.render(this) : 'VelBasic'"
10
- v-model="form[item.key]"
11
- :type="item.type || 'text'"
12
- :error="form.errors"
13
- :name="item.key"
14
- :placeholder="
15
- item.placeholder ||
16
- item.label ||
17
- item.key[0].toUpperCase() + item.key.slice(1)
18
- "
19
- :label="
20
- item.label || item.key[0].toUpperCase() + item.key.slice(1)
21
- "
22
- v-bind="item"
23
- />
24
- </template>
10
+ <legend v-if="segment.group">
11
+ {{ segment.group.title || formatGroupKey(segment.groupKey) }}
12
+ </legend>
13
+ <div :class="segment.group?.class" :style="segment.group?.style">
14
+ <component
15
+ v-for="(item, iIndex) in segment.items"
16
+ :key="`${sIndex}-${iIndex}`"
17
+ :is="item.render ? item.render(_self) : 'VelBasic'"
18
+ v-model="form[item.key]"
19
+ :type="item.type || 'text'"
20
+ :error="form.errors"
21
+ :name="item.key"
22
+ :placeholder="
23
+ item.placeholder ||
24
+ item.label ||
25
+ item.key[0].toUpperCase() + item.key.slice(1)
26
+ "
27
+ :label="
28
+ item.label ||
29
+ item.key[0].toUpperCase() + item.key.slice(1)
30
+ "
31
+ v-bind="item"
32
+ />
33
+ </div>
34
+ </component>
25
35
 
26
36
  <VelFormFooter :loading="form.processing" />
27
37
  </form>
@@ -57,5 +67,45 @@ export default {
57
67
  default: null,
58
68
  },
59
69
  },
70
+
71
+ computed: {
72
+ _self() {
73
+ return this;
74
+ },
75
+ segments() {
76
+ const items = this.resource.form.structure(this);
77
+ const groups = this.resource.form.groups || {};
78
+ const segments = [];
79
+ const groupSegments = {};
80
+
81
+ for (const item of items) {
82
+ if (item.groupKey && groups[item.groupKey]) {
83
+ if (groupSegments[item.groupKey]) {
84
+ groupSegments[item.groupKey].items.push(item);
85
+ } else {
86
+ const segment = {
87
+ groupKey: item.groupKey,
88
+ group: groups[item.groupKey],
89
+ items: [item],
90
+ };
91
+ groupSegments[item.groupKey] = segment;
92
+ segments.push(segment);
93
+ }
94
+ } else {
95
+ segments.push({ group: null, items: [item] });
96
+ }
97
+ }
98
+
99
+ return segments;
100
+ },
101
+ },
102
+
103
+ methods: {
104
+ formatGroupKey(key) {
105
+ return key
106
+ .replace(/[-_]/g, " ")
107
+ .replace(/\b\w/g, (c) => c.toUpperCase());
108
+ },
109
+ },
60
110
  };
61
111
  </script>
@@ -63,9 +63,11 @@
63
63
  </template>
64
64
 
65
65
  <script>
66
+ import { h } from "vue";
66
67
  import axios from "axios";
67
68
  import VelSpinner from "../../../components/form/Spinner.vue";
68
69
  import VelButton from "../../../components/basic/Button.vue";
70
+ import VelAudit from "../../../components/layout/Audit.vue";
69
71
  import { ElTabs, ElTabPane } from "element-plus";
70
72
 
71
73
  export default {
@@ -111,9 +113,27 @@ export default {
111
113
 
112
114
  // Compute rendered layout once
113
115
  renderedTabs() {
114
- return this.resource.show.tabs
116
+ const tabs = this.resource.show.tabs
115
117
  .map((render) => render(this))
116
118
  .filter((d) => d);
119
+
120
+ if (this.resource.auditable) {
121
+ const auditableType =
122
+ typeof this.resource.auditable === "string"
123
+ ? this.resource.auditable
124
+ : this.resource.singular.replace(/ /g, "_");
125
+
126
+ tabs.push({
127
+ label: "History",
128
+ icon: "icon-time",
129
+ component: h(VelAudit, {
130
+ auditableType,
131
+ auditableId: this.model.id,
132
+ }),
133
+ });
134
+ }
135
+
136
+ return tabs;
117
137
  },
118
138
 
119
139
  // Compute rendered actions once
@@ -129,11 +149,13 @@ export default {
129
149
  return;
130
150
  }
131
151
 
152
+ const showParams = this.resource.api.params.show(this);
153
+
132
154
  axios
133
155
  .get(
134
156
  `${this.resource.api.endpoint(this)}/${this.$route.params[`${this.resource.id}`]}`,
135
157
  {
136
- params: this.resource.api.params.show(this),
158
+ params: showParams,
137
159
  },
138
160
  )
139
161
  .then((res) => {