@fishawack/lab-velocity 2.0.0-beta.12 → 2.0.0-beta.14

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/README.md CHANGED
@@ -219,3 +219,195 @@ HYDRATE_LOGO_REVERSE Name of the logo when color scheme is reversed (default: to
219
219
  HYDRATE_REDIRECT Name of the route name to redirect to after login (default: index)
220
220
  HYDRATE_CONTACT Email for contact us button (default: mailto:det@avalerehealth.com)
221
221
  ```
222
+
223
+ ## Resources
224
+
225
+ To reduce the amount of template code needed you can import a fairly standard set of route files for index, show, create, edit & form via the resource module.
226
+
227
+ ##### routes.js
228
+
229
+ ```js
230
+ import { Resource } from "@fishawack/lab-velocity";
231
+
232
+ [
233
+ // ... other routes
234
+ ...Resource.routes(node, "posts"),
235
+ ];
236
+ ```
237
+
238
+ A third optional object can be passed for meta data that contains all of the overrides to customize the full rendering of these resource files. The default meta object is below.
239
+
240
+ ```js
241
+ // Name is the first param, properties is the optional second param
242
+ export function meta(name, properties = {}) {
243
+ const singular = properties.singular || name.slice(0, -1);
244
+
245
+ return merge(
246
+ {
247
+ name,
248
+ title: properties.title || name[0].toUpperCase() + name.slice(1),
249
+ singular,
250
+ label: singular,
251
+ multiLabel: name,
252
+ pageLink: name,
253
+ api: `/api/${name}`,
254
+ permissions: {
255
+ create: () => true,
256
+ edit: () => true,
257
+ },
258
+ searchable: {
259
+ value: "name",
260
+ label: `Search ${name}`,
261
+ },
262
+ form: {
263
+ component: null,
264
+ fields: () => ({}),
265
+ },
266
+ table: {
267
+ structure: [
268
+ {
269
+ key: "id",
270
+ },
271
+ ],
272
+ },
273
+ description: {
274
+ structure: [
275
+ {
276
+ key: "id",
277
+ },
278
+ ],
279
+ },
280
+ index: {
281
+ actions: [],
282
+ layout: [
283
+ (props) => {
284
+ const { resource } = props;
285
+
286
+ return h(VTableSorter, {
287
+ key: "PIndex",
288
+ "json-data": {
289
+ ...resource,
290
+ tableStructure: resource.table.structure,
291
+ },
292
+ defaults: resource.defaults,
293
+ "fixed-height": false,
294
+ "display-edit-action":
295
+ resource.permissions.create(props),
296
+ });
297
+ },
298
+ ],
299
+ },
300
+ show: {
301
+ actions: [],
302
+ layout: [
303
+ (props) => {
304
+ const { resource, model } = props;
305
+
306
+ return h(
307
+ ElDescriptions,
308
+ {
309
+ border: true,
310
+ column: 1,
311
+ },
312
+ resource.description.structure.map((item, index) =>
313
+ h(
314
+ ElDescriptionsItem,
315
+ {
316
+ key: index,
317
+ labelWidth: "20%",
318
+ },
319
+ {
320
+ label: () =>
321
+ item.label ||
322
+ item.key[0].toUpperCase() +
323
+ item.key.slice(1),
324
+ default: () =>
325
+ item.render
326
+ ? h(item.render(props))
327
+ : model?.[item.key] || "",
328
+ },
329
+ ),
330
+ ),
331
+ );
332
+ },
333
+ ],
334
+ },
335
+ defaults: "",
336
+ icon: `icon-${singular}`,
337
+ },
338
+ properties,
339
+ );
340
+ }
341
+ ```
342
+
343
+ Layout is an array of functions that return render functions
344
+
345
+ ```js
346
+ import { h, resolveComponent } from "vue";
347
+
348
+ {
349
+ // ...
350
+ show: {
351
+ layout: [
352
+ ({ model }) =>
353
+ h(resolveComponent("router-link"), {
354
+ class: "underline",
355
+ to: {
356
+ name: "companies.show",
357
+ params: { id: model.company_id },
358
+ },
359
+ text: model.company.name,
360
+ })
361
+ ],
362
+ },
363
+ }
364
+ ```
365
+
366
+ You can see above that the show & index route have a default render function returned for a table and description block. If you want to keep this you can grab the existing render function from the `defaultResource` export.
367
+
368
+ ```js
369
+ import { defaultResource, meta } from "../../../resource/index.js";
370
+
371
+ {
372
+ // ...
373
+ index: {
374
+ layout: [
375
+ () => h("div", "I appear above"),
376
+ ...defaultResource.index.layout,
377
+ () => h("div", "I appear below"),
378
+ ],
379
+ },
380
+ }
381
+ ```
382
+
383
+ Structure arrays take objects. The objects require a key only but have other optional properties. A render function can also be passed to fully customize the rendering that happens.
384
+
385
+ ```js
386
+ {
387
+ table: {
388
+ structure: [
389
+ {
390
+ key: "name",
391
+ sortable: true,
392
+ },
393
+ {
394
+ key: "role",
395
+ render: (row) => h("div", "Custom template"),
396
+ },
397
+ ],
398
+ },
399
+ form: {
400
+ structure: [
401
+ {
402
+ key: "name",
403
+ },
404
+ {
405
+ key: "provider_name",
406
+ label: "Provider",
407
+ render: ({ model }) => h("span", model?.provider_name.label ?? ""),
408
+ initial: ({ model }) => model?.provider_name.value ?? null,
409
+ }
410
+ ]
411
+ }
412
+ }
413
+ ```
@@ -1,13 +1,15 @@
1
1
  <template>
2
2
  <div class="vel-app">
3
3
  <VelHeader class="justify-end-dive">
4
- <GSvg
5
- class="logo"
6
- style="width: 180px"
7
- embed
8
- asis
9
- :name="$store.state.auth.logo"
10
- />
4
+ <router-link :to="{ name: 'index' }">
5
+ <GSvg
6
+ class="logo"
7
+ style="width: 180px"
8
+ embed
9
+ asis
10
+ :name="$store.state.auth.logo"
11
+ />
12
+ </router-link>
11
13
  <template #links>
12
14
  <div class="flex items-center pr">
13
15
  <GIcon
@@ -6,7 +6,7 @@ import VelTableSorter from "../../../../components/layout/TableSorter.vue";
6
6
  import VelRoleLegend from "../../../../components/layout/RoleLegend.vue";
7
7
  import component from "./form.vue";
8
8
  import userResource from "../PUsers/resource.js";
9
- import { meta } from "../../../resource/index.js";
9
+ import { defaultResource, meta } from "../../../resource/index.js";
10
10
 
11
11
  import { ElNotification } from "element-plus";
12
12
  import { h } from "vue";
@@ -16,6 +16,10 @@ export default [
16
16
  "companies",
17
17
  {
18
18
  defaults: "include=primary_contact",
19
+ permissions: {
20
+ create: ({ $store }) => $store.getters.can("write companies"),
21
+ edit: ({ $store }) => $store.getters.can("write companies"),
22
+ },
19
23
  singular: "company",
20
24
  icon: "icon-cases",
21
25
  form: {
@@ -34,18 +38,16 @@ export default [
34
38
  table: {
35
39
  structure: [
36
40
  {
37
- label: "Name",
38
41
  key: "name",
39
42
  sortable: true,
40
43
  },
41
44
  {
42
- label: "Total users",
43
45
  key: "user_count",
44
- sortable: false,
46
+ label: "Total users",
45
47
  width: "150",
46
48
  },
47
49
  {
48
- label: "Role",
50
+ key: "role",
49
51
  render: (row) =>
50
52
  h(
51
53
  row.roles.length === 1 ? Chip : Chips,
@@ -59,120 +61,104 @@ export default [
59
61
  },
60
62
  ],
61
63
  },
62
- index: {
64
+ description: {
63
65
  structure: [
64
66
  {
65
- render: ({ resource, $store }) =>
66
- h(VelTableSorter, {
67
- key: "PIndex",
68
- "json-data": {
69
- ...resource,
70
- tableStructure: resource.table.structure,
71
- },
72
- defaults: resource.defaults,
73
- "fixed-height": false,
74
- "display-edit-action":
75
- $store.getters.can("write companies"),
76
- }),
67
+ key: "domains",
68
+ render: ({ model }) => h("span", model.domains.join(", ")),
77
69
  },
78
70
  {
79
- render: () =>
80
- h(VelRoleLegend, {
81
- class: "mt-5",
82
- }),
71
+ key: "sso_enabled",
72
+ label: "SSO Enabled",
83
73
  },
84
- ],
85
- },
86
- show: {
87
- actions: [
88
74
  {
75
+ key: "primary_contact",
76
+ label: "Primary Contact",
89
77
  render: ({ model }) =>
90
- model.primary_contact &&
91
- h(
92
- VelButton,
93
- {
94
- type: "primary",
95
- async onClick() {
96
- try {
97
- const res = await axios.post(
98
- `/api/companies/${model.id}/welcome`,
99
- );
100
- ElNotification({
101
- title: "Success",
102
- message: res.data.message,
103
- type: "success",
104
- });
105
-
106
- model.primary_contact_contacted = true;
107
- } catch (e) {
108
- ElNotification({
109
- title: "Warning",
110
- message:
111
- e.response?.data?.message ||
112
- e.message,
113
- type: "warning",
114
- });
115
- }
116
- },
117
- },
118
- "Send welcome email",
119
- ),
78
+ h("span", model.primary_contact?.name),
120
79
  },
121
- ],
122
- structure: [
123
- [
124
- {
125
- label: "Domains",
126
- render: ({ model }) =>
127
- h("span", model.domains.join(", ")),
128
- },
129
- {
130
- label: "SSO Enabled",
131
- key: "sso_enabled",
132
- },
133
- {
134
- label: "Primary Contact",
135
- render: ({ model }) =>
136
- h("span", model.primary_contact?.name),
137
- },
138
- {
139
- label: "Primary Contact Email",
140
- render: ({ model }) =>
141
- h("span", model.primary_contact?.email),
142
- },
143
- {
144
- label: "Primary Contact Contacted",
145
- render: ({ model }) =>
146
- h("span", !!model.primary_contact_contacted),
147
- },
148
- {
149
- label: "Total users",
150
- key: "user_count",
151
- },
152
- ],
153
80
  {
81
+ key: "primary_contact_email",
82
+ label: "Primary Contact Email",
154
83
  render: ({ model }) =>
155
- h(VelFormRole, {
156
- overrides: model.overrides_roles_and_permissions,
157
- form: { roles: model.roles.map((d) => d.id) },
158
- readonly: true,
159
- }),
84
+ h("span", model.primary_contact?.email),
160
85
  },
161
86
  {
162
- render: ({ model, $store }) => {
163
- const resource = meta(...userResource);
164
- return h(VelTableSorter, {
165
- key: "PIndex",
166
- "json-data": {
167
- ...resource,
168
- tableStructure: resource.table.structure,
87
+ key: "primary_contact_contacted",
88
+ label: "Primary Contact Contacted",
89
+ render: ({ model }) =>
90
+ h("span", !!model.primary_contact_contacted),
91
+ },
92
+ {
93
+ label: "Total users",
94
+ key: "user_count",
95
+ },
96
+ ],
97
+ },
98
+ index: {
99
+ layout: [
100
+ ...defaultResource.index.layout,
101
+ () =>
102
+ h(VelRoleLegend, {
103
+ class: "mt-5",
104
+ }),
105
+ ],
106
+ },
107
+ show: {
108
+ actions: [
109
+ ({ model }) =>
110
+ model.primary_contact &&
111
+ h(
112
+ VelButton,
113
+ {
114
+ type: "primary",
115
+ async onClick() {
116
+ try {
117
+ const res = await axios.post(
118
+ `/api/companies/${model.id}/welcome`,
119
+ );
120
+ ElNotification({
121
+ title: "Success",
122
+ message: res.data.message,
123
+ type: "success",
124
+ });
125
+
126
+ model.primary_contact_contacted = true;
127
+ } catch (e) {
128
+ ElNotification({
129
+ title: "Warning",
130
+ message:
131
+ e.response?.data?.message ||
132
+ e.message,
133
+ type: "warning",
134
+ });
135
+ }
169
136
  },
170
- defaults: `include=company&filter[company_id]=${model.id}`,
171
- "fixed-height": false,
172
- "display-edit-action":
173
- $store.getters.can("write users"),
174
- });
175
- },
137
+ },
138
+ "Send welcome email",
139
+ ),
140
+ ],
141
+ layout: [
142
+ ...defaultResource.show.layout,
143
+ ({ model }) =>
144
+ h(VelFormRole, {
145
+ overrides: model.overrides_roles_and_permissions,
146
+ form: { roles: model.roles.map((d) => d.id) },
147
+ readonly: true,
148
+ }),
149
+ ({ model, $store }) => {
150
+ const resource = meta(...userResource);
151
+ return h(VelTableSorter, {
152
+ key: "PIndex",
153
+ "json-data": {
154
+ ...resource,
155
+ tableStructure: resource.table.structure,
156
+ },
157
+ defaults: `include=company&filter[company_id]=${model.id}`,
158
+ "fixed-height": false,
159
+ "display-edit-action":
160
+ $store.getters.can("write users"),
161
+ });
176
162
  },
177
163
  ],
178
164
  },
@@ -4,6 +4,7 @@ import Chips from "../../../../components/layout/Chips.vue";
4
4
  import VelTableSorter from "../../../../components/layout/TableSorter.vue";
5
5
  import VelRoleLegend from "../../../../components/layout/RoleLegend.vue";
6
6
  import component from "./form.vue";
7
+ import { defaultResource } from "../../../resource/index.js";
7
8
 
8
9
  import { ElMessageBox } from "element-plus";
9
10
  import { ElNotification } from "element-plus";
@@ -27,6 +28,10 @@ export default [
27
28
  searchable: {
28
29
  value: "email",
29
30
  },
31
+ permissions: {
32
+ create: ({ $store }) => $store.getters.can("write users"),
33
+ edit: ({ $store }) => $store.getters.can("write users"),
34
+ },
30
35
  form: {
31
36
  async submit({ model, form, $router, $store, method }) {
32
37
  try {
@@ -111,17 +116,14 @@ export default [
111
116
  table: {
112
117
  structure: [
113
118
  {
114
- label: "Name",
115
119
  key: "name",
116
120
  sortable: true,
117
121
  },
118
122
  {
119
- label: "Email",
120
123
  key: "email",
121
- sortable: true,
122
124
  },
123
125
  {
124
- label: "Company",
126
+ key: "company",
125
127
  sortable: true,
126
128
  render: (model) =>
127
129
  h(resolveComponent("router-link"), {
@@ -134,7 +136,7 @@ export default [
134
136
  }),
135
137
  },
136
138
  {
137
- label: "Role",
139
+ key: "role",
138
140
  render: (row) =>
139
141
  h(
140
142
  !row.overrides_roles_and_permissions ||
@@ -156,58 +158,43 @@ export default [
156
158
  },
157
159
  ],
158
160
  },
159
- index: {
161
+ description: {
160
162
  structure: [
161
163
  {
162
- render: ({ resource, $store }) =>
163
- h(VelTableSorter, {
164
- key: "PIndex",
165
- "json-data": {
166
- ...resource,
167
- tableStructure: resource.table.structure,
168
- },
169
- defaults: resource.defaults,
170
- "fixed-height": false,
171
- "display-edit-action":
172
- $store.getters.can("write users"),
173
- }),
164
+ key: "email",
174
165
  },
175
166
  {
176
- render: () =>
177
- h(VelRoleLegend, {
178
- class: "mt-5",
167
+ key: "company",
168
+ render: ({ model }) =>
169
+ h(resolveComponent("router-link"), {
170
+ class: "underline",
171
+ to: {
172
+ name: "companies.show",
173
+ params: { id: model.company_id },
174
+ },
175
+ text: model.company.name,
179
176
  }),
180
177
  },
181
178
  ],
182
179
  },
180
+ index: {
181
+ layout: [
182
+ ...defaultResource.index.layout,
183
+ () =>
184
+ h(VelRoleLegend, {
185
+ class: "mt-5",
186
+ }),
187
+ ],
188
+ },
183
189
  show: {
184
- structure: [
185
- [
186
- {
187
- label: "Email",
188
- key: "email",
189
- },
190
- {
191
- label: "Company",
192
- render: ({ model }) =>
193
- h(resolveComponent("router-link"), {
194
- class: "underline",
195
- to: {
196
- name: "companies.show",
197
- params: { id: model.company_id },
198
- },
199
- text: model.company.name,
200
- }),
201
- },
202
- ],
203
- {
204
- render: ({ model }) =>
205
- h(VelFormRole, {
206
- overrides: model.overrides_roles_and_permissions,
207
- form: { roles: model.roles.map((d) => d.id) },
208
- readonly: true,
209
- }),
210
- },
190
+ layout: [
191
+ ...defaultResource.show.layout,
192
+ ({ model }) =>
193
+ h(VelFormRole, {
194
+ overrides: model.overrides_roles_and_permissions,
195
+ form: { roles: model.roles.map((d) => d.id) },
196
+ readonly: true,
197
+ }),
211
198
  ],
212
199
  },
213
200
  },
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <VBreadcrumbs :items="breadcrumbs" class="mb-8" container-classes="m-0" />
3
+
4
+ <div class="container px-6 tablet:px-4 mobile:px-2 mb-8 ml-0 mr-0">
5
+ <div class="grid__1/1">
6
+ <div class="grid__1/1 mb-4">
7
+ <h2 class="h1">Create {{ resource.singular }}</h2>
8
+ </div>
9
+ <div class="mt grid__1/2">
10
+ <component
11
+ :is="resource.form.component ?? 'XForm'"
12
+ ref="form"
13
+ :form="form"
14
+ :submit="submit"
15
+ :method="method"
16
+ :resource="resource"
17
+ />
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </template>
22
+
23
+ <script>
24
+ import Form from "form-backend-validation";
25
+
26
+ export default {
27
+ components: {
28
+ VBreadcrumbs: require("../../../components/layout/Breadcrumbs.vue")
29
+ .default,
30
+ XForm: require("./partials/form.vue").default,
31
+ },
32
+
33
+ props: {
34
+ breadcrumbs: {
35
+ type: Array,
36
+ required: true,
37
+ },
38
+ resource: {
39
+ type: Object,
40
+ required: true,
41
+ },
42
+ },
43
+
44
+ data() {
45
+ return {
46
+ form: null,
47
+ method: "post",
48
+ };
49
+ },
50
+
51
+ beforeMount() {
52
+ this.form = new Form(this.resource.form.fields(this), {
53
+ resetOnSuccess: false,
54
+ });
55
+ },
56
+
57
+ methods: {
58
+ async submit() {
59
+ if (this.resource.form.submit) {
60
+ await this.resource.form.submit(this);
61
+ } else {
62
+ try {
63
+ let res = await this.form.post(`${this.resource.api}`);
64
+
65
+ this.$router.replace({
66
+ name: `${this.resource.name}.show`,
67
+ params: { id: res.data.id },
68
+ });
69
+ } catch (e) {
70
+ console.log(e);
71
+ }
72
+ }
73
+ },
74
+ },
75
+ };
76
+ </script>
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <VBreadcrumbs
3
+ :items="addBreadcrumbs"
4
+ class="mb-8"
5
+ container-classes="m-0"
6
+ />
7
+
8
+ <div class="container px-6 tablet:px-4 mobile:px-2 mb-8 ml-0 mr-0">
9
+ <div class="grid__1/1">
10
+ <div class="grid__1/1 mb-4">
11
+ <h2 class="h1">Edit {{ resource.singular }}</h2>
12
+ </div>
13
+ <div class="grid__1/2">
14
+ <component
15
+ :is="resource.form.component ?? 'XForm'"
16
+ ref="form"
17
+ :form="form"
18
+ :submit="submit"
19
+ :method="method"
20
+ :resource="resource"
21
+ />
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script>
28
+ import Form from "form-backend-validation";
29
+
30
+ export default {
31
+ components: {
32
+ VBreadcrumbs: require("../../../components/layout/Breadcrumbs.vue")
33
+ .default,
34
+ XForm: require("./partials/form.vue").default,
35
+ },
36
+
37
+ props: {
38
+ breadcrumbs: {
39
+ type: Array,
40
+ required: true,
41
+ },
42
+ resource: {
43
+ type: Object,
44
+ required: true,
45
+ },
46
+ },
47
+
48
+ data() {
49
+ return {
50
+ form: null,
51
+ model: null,
52
+ addBreadcrumbs: [...this.$props.breadcrumbs],
53
+ method: "patch",
54
+ };
55
+ },
56
+
57
+ beforeMount() {
58
+ this.form = new Form(this.resource.form.fields(this), {
59
+ resetOnSuccess: false,
60
+ });
61
+ },
62
+
63
+ async mounted() {
64
+ window.axios
65
+ .get(
66
+ `${this.resource.api}/${this.$route.params.id}?${this.resource.defaults}`,
67
+ )
68
+ .then((res) => {
69
+ this.model = res.data.data;
70
+
71
+ // Set initial form data
72
+ Object.entries(this.resource.form.fields(this)).forEach(
73
+ ([key, value]) => {
74
+ this.form[key] = value;
75
+ },
76
+ );
77
+
78
+ this.addBreadcrumbs.push({
79
+ href: {
80
+ name: `${this.resource.name}.show`,
81
+ param: this.model.id,
82
+ },
83
+ text: this.model.name,
84
+ });
85
+ });
86
+ },
87
+
88
+ methods: {
89
+ async submit() {
90
+ if (this.resource.form.submit) {
91
+ await this.resource.form.submit(this);
92
+ } else {
93
+ try {
94
+ let res = await this.form.patch(
95
+ `${this.resource.api}/${this.model.id}`,
96
+ );
97
+
98
+ this.$router.replace({
99
+ name: `${this.resource.name}.show`,
100
+ params: { id: res.data.id },
101
+ });
102
+ } catch (e) {
103
+ console.log(e);
104
+ }
105
+ }
106
+ },
107
+ },
108
+ };
109
+ </script>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <VBreadcrumbs :items="breadcrumbs" />
3
+
4
+ <div class="container px-6 tablet:px-4 mobile:px-2 mb-8 ml-0 mr-0">
5
+ <div class="grid__1/1">
6
+ <h2 class="h1 pb-4">
7
+ {{ breadcrumbs[breadcrumbs.length - 1].text }}
8
+ </h2>
9
+
10
+ <div class="flex gap items-center justify-end">
11
+ <template
12
+ v-for="(render, index) in resource.index.actions"
13
+ :key="index"
14
+ >
15
+ <component :is="render(this)" />
16
+ </template>
17
+ </div>
18
+
19
+ <template
20
+ v-for="(render, index) in resource.index.layout"
21
+ :key="index"
22
+ >
23
+ <component :is="render(this)" />
24
+ </template>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <script>
30
+ export default {
31
+ components: {
32
+ VBreadcrumbs: require("../../../components/layout/Breadcrumbs.vue")
33
+ .default,
34
+ },
35
+
36
+ props: {
37
+ breadcrumbs: {
38
+ type: Array,
39
+ required: true,
40
+ },
41
+ resource: {
42
+ type: Object,
43
+ required: true,
44
+ },
45
+ },
46
+ };
47
+ </script>
@@ -0,0 +1,53 @@
1
+ <!-- eslint-disable vue/no-mutating-props -->
2
+ <template>
3
+ <form @submit.prevent="submit">
4
+ <template v-for="(item, index) in resource.form.structure" :key="index">
5
+ <component
6
+ :is="item.render ? item.render(this) : 'VelBasic'"
7
+ v-model="form[item.key]"
8
+ :type="item.type || 'text'"
9
+ :error="form.errors"
10
+ :name="item.key"
11
+ :placeholder="
12
+ item.placeholder ||
13
+ item.key[0].toUpperCase() + item.key.slice(1)
14
+ "
15
+ :label="
16
+ item.label || item.key[0].toUpperCase() + item.key.slice(1)
17
+ "
18
+ v-bind="item"
19
+ />
20
+ </template>
21
+
22
+ <VelFormFooter :loading="form.processing" />
23
+ </form>
24
+ </template>
25
+
26
+ <script>
27
+ export default {
28
+ components: {
29
+ VelFormFooter: require("../../../../components/layout/FormFooter.vue")
30
+ .default,
31
+ VelBasic: require("../../../../components/form/basic.vue").default,
32
+ },
33
+
34
+ props: {
35
+ form: {
36
+ required: true,
37
+ type: Object,
38
+ },
39
+ submit: {
40
+ required: true,
41
+ type: Function,
42
+ },
43
+ method: {
44
+ type: String,
45
+ default: "post",
46
+ },
47
+ resource: {
48
+ required: true,
49
+ type: Object,
50
+ },
51
+ },
52
+ };
53
+ </script>
@@ -0,0 +1,116 @@
1
+ <template>
2
+ <VBreadcrumbs
3
+ :items="addBreadcrumbs"
4
+ class="mb-8"
5
+ container-classes="m-0"
6
+ />
7
+
8
+ <div class="container px-6 tablet:px-4 mobile:px-2 mb-8 ml-0 mr-0">
9
+ <div class="grid__1/1">
10
+ <template v-if="model">
11
+ <div class="bg-0 p-3 box-shadow-1 border-r-4 mb-6">
12
+ <VelPageHeader
13
+ :icon="resource.icon"
14
+ :title="`${model.name} ${model.last_name ?? ''}`"
15
+ >
16
+ <template
17
+ v-for="(render, index) in resource.show.actions"
18
+ :key="index"
19
+ >
20
+ <component :is="render(this)" />
21
+ </template>
22
+ <VelButton
23
+ v-if="resource.permissions.edit(this)"
24
+ tag="a"
25
+ type="primary"
26
+ @click="
27
+ $router.push({
28
+ name: `${resource.name}.edit`,
29
+ param: model.id,
30
+ })
31
+ "
32
+ >
33
+ <GIcon
34
+ class="fill-0 mr-0.5 icon--0.5"
35
+ name="icon-edit"
36
+ embed
37
+ artboard
38
+ />
39
+ Edit {{ resource.singular }}
40
+ </VelButton>
41
+ </VelPageHeader>
42
+
43
+ <hr class="my-3 hr-muted" />
44
+
45
+ <template
46
+ v-for="(render, index) in resource.show.layout"
47
+ :key="index"
48
+ >
49
+ <component :is="render(this)" />
50
+
51
+ <hr
52
+ v-if="index < resource.show.layout.length - 1"
53
+ class="my-3 hr-muted"
54
+ />
55
+ </template>
56
+ </div>
57
+ </template>
58
+
59
+ <div v-else class="absolute transform-center text-center">
60
+ <VelSpinner />
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </template>
65
+
66
+ <script>
67
+ import axios from "axios";
68
+ import VelSpinner from "../../../components/form/Spinner.vue";
69
+ import VelButton from "../../../components/basic/Button.vue";
70
+
71
+ export default {
72
+ components: {
73
+ VBreadcrumbs: require("../../../components/layout/Breadcrumbs.vue")
74
+ .default,
75
+ VelPageHeader: require("../../../components/layout/PageHeader.vue")
76
+ .default,
77
+ VelSpinner,
78
+ VelButton,
79
+ },
80
+
81
+ props: {
82
+ breadcrumbs: {
83
+ type: Array,
84
+ required: true,
85
+ },
86
+ resource: {
87
+ type: Object,
88
+ required: true,
89
+ },
90
+ },
91
+
92
+ data() {
93
+ return {
94
+ model: null,
95
+ addBreadcrumbs: [...this.$props.breadcrumbs],
96
+ };
97
+ },
98
+
99
+ mounted() {
100
+ axios
101
+ .get(
102
+ `${this.resource.api}/${this.$route.params.id}?${this.resource.defaults}`,
103
+ )
104
+ .then((res) => {
105
+ this.model = res.data.data;
106
+ this.addBreadcrumbs.push({
107
+ href: {
108
+ name: `${this.resource.name}.show`,
109
+ param: this.model.id,
110
+ },
111
+ text: this.model.name,
112
+ });
113
+ });
114
+ },
115
+ };
116
+ </script>
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+
3
+ import { merge } from "lodash";
4
+
5
+ import { h } from "vue";
6
+
7
+ import VTableSorter from "../../components/layout/TableSorter.vue";
8
+ import { ElDescriptions, ElDescriptionsItem } from "element-plus";
9
+
10
+ export const defaultResource = meta();
11
+
12
+ export function meta(name = "default", properties = {}) {
13
+ const singular = properties.singular || name.slice(0, -1);
14
+
15
+ return merge(
16
+ {
17
+ name,
18
+ title: properties.title || name[0].toUpperCase() + name.slice(1),
19
+ singular,
20
+ label: singular,
21
+ multiLabel: name,
22
+ pageLink: name,
23
+ api: `/api/${name}`,
24
+ permissions: {
25
+ create: () => true,
26
+ edit: () => true,
27
+ },
28
+ searchable: {
29
+ value: "name",
30
+ label: `Search ${name}`,
31
+ },
32
+ form: {
33
+ component: null,
34
+ fields: () => ({}),
35
+ structure: [],
36
+ },
37
+ table: {
38
+ structure: [
39
+ {
40
+ key: "id",
41
+ },
42
+ ],
43
+ },
44
+ description: {
45
+ structure: [
46
+ {
47
+ key: "id",
48
+ },
49
+ ],
50
+ },
51
+ index: {
52
+ actions: [],
53
+ layout: [
54
+ (props) => {
55
+ const { resource } = props;
56
+
57
+ return h(VTableSorter, {
58
+ key: "PIndex",
59
+ "json-data": {
60
+ ...resource,
61
+ tableStructure: resource.table.structure,
62
+ },
63
+ defaults: resource.defaults,
64
+ "fixed-height": false,
65
+ "display-edit-action":
66
+ resource.permissions.create(props),
67
+ });
68
+ },
69
+ ],
70
+ },
71
+ show: {
72
+ actions: [],
73
+ layout: [
74
+ (props) => {
75
+ const { resource, model } = props;
76
+
77
+ return h(
78
+ ElDescriptions,
79
+ {
80
+ border: true,
81
+ column: 1,
82
+ },
83
+ resource.description.structure.map((item, index) =>
84
+ h(
85
+ ElDescriptionsItem,
86
+ {
87
+ key: index,
88
+ labelWidth: "20%",
89
+ },
90
+ {
91
+ label: () =>
92
+ item.label ||
93
+ item.key[0].toUpperCase() +
94
+ item.key.slice(1),
95
+ default: () =>
96
+ item.render
97
+ ? h(item.render(props))
98
+ : model?.[item.key] || "",
99
+ },
100
+ ),
101
+ ),
102
+ );
103
+ },
104
+ ],
105
+ },
106
+ defaults: "",
107
+ icon: `icon-${singular}`,
108
+ },
109
+ properties,
110
+ );
111
+ }
112
+
113
+ // Export resource
114
+ export function routes(node, name, properties = {}) {
115
+ return [
116
+ {
117
+ path: `/${name}`,
118
+ component: node ? "" : require("../resource/parent.vue").default,
119
+ meta: {
120
+ resource: meta(name, properties),
121
+ },
122
+ children: [
123
+ {
124
+ path: "",
125
+ component: node
126
+ ? ""
127
+ : require("../resource/Children/index.vue").default,
128
+ name: `${name}.index`,
129
+ },
130
+ {
131
+ path: "create",
132
+ component: node
133
+ ? ""
134
+ : require("../resource/Children/create.vue").default,
135
+ name: `${name}.create`,
136
+ },
137
+ {
138
+ path: ":id",
139
+ component: node
140
+ ? ""
141
+ : require("../resource/Children/show.vue").default,
142
+ name: `${name}.show`,
143
+ },
144
+ {
145
+ path: ":id/edit",
146
+ component: node
147
+ ? ""
148
+ : require("../resource/Children/edit.vue").default,
149
+ name: `${name}.edit`,
150
+ },
151
+ ],
152
+ },
153
+ ];
154
+ }
155
+
156
+ export default {
157
+ routes,
158
+ meta,
159
+ };
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <PageTitle :title="resource.title" />
3
+
4
+ <router-view
5
+ :key="$route.path"
6
+ :breadcrumbs="breadcrumbs"
7
+ :resource="resource"
8
+ />
9
+ </template>
10
+
11
+ <script>
12
+ import PageTitle from "../../components/layout/pageTitle.vue";
13
+
14
+ export default {
15
+ components: {
16
+ PageTitle,
17
+ },
18
+
19
+ computed: {
20
+ resource() {
21
+ return this.$route.meta.resource;
22
+ },
23
+ breadcrumbs() {
24
+ return [
25
+ {
26
+ href: {
27
+ name: "index",
28
+ },
29
+ text: "Home",
30
+ },
31
+ {
32
+ href: {
33
+ name: `${this.resource.name}.index`,
34
+ },
35
+ text: this.resource.title,
36
+ },
37
+ ];
38
+ },
39
+ },
40
+ };
41
+ </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishawack/lab-velocity",
3
- "version": "2.0.0-beta.12",
3
+ "version": "2.0.0-beta.14",
4
4
  "description": "Avalere Health branded style system",
5
5
  "scripts": {
6
6
  "setup": "npm ci || npm i && npm run content",
@@ -64,7 +64,8 @@
64
64
  "*.scss",
65
65
  "components",
66
66
  "_Build/vue/components",
67
- "_Build/vue/modules/AuthModule"
67
+ "_Build/vue/modules/AuthModule",
68
+ "_Build/vue/modules/resource"
68
69
  ],
69
70
  "main": "index.js",
70
71
  "release": {