@fishawack/lab-velocity 2.0.0-beta.23 → 2.0.0-beta.25

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.
@@ -23,7 +23,12 @@
23
23
  <!-- Support a custom render function -->
24
24
  <template v-if="item.render" #default="scope">
25
25
  <component
26
- :is="item.render({ model: scope.row, ...this })"
26
+ :is="
27
+ item.render({
28
+ model: scope.row,
29
+ $emit,
30
+ })
31
+ "
27
32
  />
28
33
  </template>
29
34
  </el-table-column>
@@ -118,7 +123,7 @@ export default {
118
123
  default: true,
119
124
  },
120
125
  },
121
- emits: ["sort"],
126
+ emits: ["sort", "reload"],
122
127
 
123
128
  methods: {
124
129
  handleSort(data) {
@@ -67,19 +67,20 @@
67
67
  :target-action="
68
68
  (item) => ({
69
69
  name: `${jsonData.slug}.show`,
70
- params: { id: item.id },
70
+ params: { [idKey]: item.id },
71
71
  })
72
72
  "
73
73
  :edit-action="
74
74
  (item) => ({
75
75
  name: `${jsonData.slug}.edit`,
76
- params: { id: item.id },
76
+ params: { [idKey]: item.id },
77
77
  })
78
78
  "
79
79
  :display-actions="displayActions"
80
80
  :display-show-action="displayShowAction"
81
81
  :display-edit-action="displayEditAction"
82
82
  @sort="handleSort"
83
+ @reload="reload"
83
84
  >
84
85
  <slot name="table-content"></slot>
85
86
  </VelTable>
@@ -128,10 +129,10 @@ export default {
128
129
  default: true,
129
130
  type: Boolean,
130
131
  },
131
- defaults: {
132
+ apiParams: {
132
133
  required: false,
133
- type: String,
134
- default: "",
134
+ type: Object,
135
+ default: () => ({}),
135
136
  },
136
137
  displayActions: {
137
138
  type: Boolean,
@@ -149,13 +150,17 @@ export default {
149
150
  type: Boolean,
150
151
  default: true,
151
152
  },
153
+ idKey: {
154
+ type: String,
155
+ default: "id",
156
+ },
152
157
  },
153
158
 
154
159
  data() {
155
160
  return {
156
161
  search: null,
157
162
  sort: "-id",
158
- query: [],
163
+ query: {},
159
164
  table_data: [],
160
165
  table_meta: null,
161
166
  };
@@ -172,13 +177,21 @@ export default {
172
177
  },
173
178
 
174
179
  methods: {
180
+ reload() {
181
+ this.fetchData({ page: this.table_meta.current_page }).then(
182
+ (data) => {
183
+ this.table_data = data.data;
184
+ this.table_meta = data.meta;
185
+ },
186
+ );
187
+ },
175
188
  handleSearch(data) {
176
189
  if (data === null || data.length < 3) {
177
- this.query = [];
190
+ this.query = {};
178
191
  } else {
179
- this.query = [
180
- `filter[${this.$refs.search.$el.dataset.key}]=${data}`,
181
- ];
192
+ this.query = {
193
+ [`filter[${this.$refs.search.$el.dataset.key}]`]: data,
194
+ };
182
195
  }
183
196
  this.fetchData({}).then((data) => {
184
197
  this.table_data = data.data;
@@ -199,17 +212,15 @@ export default {
199
212
  },
200
213
 
201
214
  fetchData: function ({ page = "1" }) {
202
- const mergedQuery = [
203
- ...this.query,
204
- this.defaults,
205
- `sort=${this.sort}`,
206
- `page=${page}`,
207
- ];
208
-
209
- const query = mergedQuery.join("&");
210
-
211
215
  return axios
212
- .get(`${this.$props.jsonData.api}${query ? "?" + query : ""}`)
216
+ .get(`${this.$props.jsonData.api}`, {
217
+ params: {
218
+ sort: this.sort,
219
+ page: page,
220
+ ...this.query,
221
+ ...this.apiParams,
222
+ },
223
+ })
213
224
  .then((res) => res.data)
214
225
  .catch(console.log);
215
226
  },
@@ -179,7 +179,9 @@ export default {
179
179
 
180
180
  mounted() {
181
181
  axios
182
- .getAll(`/api/users?filter[company_id]=${this.$route.params.id}`)
182
+ .getAll(
183
+ `/api/users?filter[company_id]=${this.$route.params.companiesId}`,
184
+ )
183
185
  .then((res) => {
184
186
  this.users = res.data.data;
185
187
  });
@@ -9,13 +9,17 @@ import userResource from "../PUsers/resource.js";
9
9
  import { defaultResource, meta } from "../../../resource/index.js";
10
10
 
11
11
  import { ElNotification } from "element-plus";
12
- import { h } from "vue";
12
+ import { h, resolveComponent } from "vue";
13
13
  import axios from "axios";
14
14
 
15
15
  export default [
16
16
  "companies",
17
17
  {
18
- defaults: "include=primary_contact",
18
+ api: {
19
+ params: {
20
+ show: { include: "primary_contact" },
21
+ },
22
+ },
19
23
  permissions: {
20
24
  create: ({ $store }) => $store.getters.can("write companies"),
21
25
  edit: ({ $store }) => $store.getters.can("write companies"),
@@ -148,21 +152,47 @@ export default [
148
152
  form: { roles: model.roles.map((d) => d.id) },
149
153
  readonly: true,
150
154
  }),
151
- ({ model, $store }) => {
155
+ ({ model, $store, $router, ...rest }) => {
152
156
  const resource = meta(...userResource);
153
- return h(VelTableSorter, {
154
- key: "PIndex",
155
- "json-data": {
156
- ...resource,
157
- tableStructure: resource.table.structure,
158
- },
159
- defaults: `include=company&filter[company_id]=${model.id}`,
160
- "fixed-height": false,
161
- "display-create-action":
162
- $store.getters.can("write users"),
163
- "display-edit-action":
164
- $store.getters.can("write users"),
165
- });
157
+
158
+ resource.api.params.index["filter[company_id]"] = model.id;
159
+
160
+ const props = {
161
+ model,
162
+ $store,
163
+ $router,
164
+ ...rest,
165
+ resource,
166
+ };
167
+
168
+ return h("div", [
169
+ h("div", { class: "flex justify-end items-end" }, [
170
+ resource.permissions.create(props) &&
171
+ h(
172
+ VelButton,
173
+ {
174
+ tag: "a",
175
+ type: "primary",
176
+ size: "large",
177
+ onClick: () => {
178
+ $router.push({
179
+ name: `${resource.slug}.create`,
180
+ });
181
+ },
182
+ },
183
+ () => [
184
+ h(resolveComponent("GIcon"), {
185
+ class: "fill-0 mr-0.5 icon--0.5",
186
+ name: "icon-plus",
187
+ embed: true,
188
+ artboard: true,
189
+ }),
190
+ `Create ${resource.singular}`,
191
+ ],
192
+ ),
193
+ ]),
194
+ h(VelTableSorter, resource.index.structure(props)),
195
+ ]);
166
196
  },
167
197
  ],
168
198
  },
@@ -1,10 +1,10 @@
1
1
  import VelFormRole from "../../../../components/layout/FormRole.vue";
2
2
  import Chip from "../../../../components/layout/Chip.vue";
3
3
  import Chips from "../../../../components/layout/Chips.vue";
4
- import VelTableSorter from "../../../../components/layout/TableSorter.vue";
5
4
  import VelRoleLegend from "../../../../components/layout/RoleLegend.vue";
6
5
  import component from "./form.vue";
7
- import { defaultResource } from "../../../resource/index.js";
6
+ import companyResource from "../PCompanies/resource.js";
7
+ import { defaultResource, meta } from "../../../resource/index.js";
8
8
 
9
9
  import { ElMessageBox } from "element-plus";
10
10
  import { ElNotification } from "element-plus";
@@ -24,7 +24,12 @@ function generatePassword(
24
24
  export default [
25
25
  "users",
26
26
  {
27
- defaults: "include=company",
27
+ api: {
28
+ params: {
29
+ index: { include: "company" },
30
+ show: { include: "company" },
31
+ },
32
+ },
28
33
  searchable: {
29
34
  value: "email",
30
35
  },
@@ -34,7 +39,7 @@ export default [
34
39
  delete: ({ $store }) => $store.getters.can("delete companies"),
35
40
  },
36
41
  form: {
37
- async submit({ model, form, $router, $store, method }) {
42
+ async submit({ model, form, $router, $store, method, resource }) {
38
43
  try {
39
44
  if (method === "post") {
40
45
  if (form.set_password) {
@@ -57,7 +62,7 @@ export default [
57
62
  .then(() => {
58
63
  $router.replace({
59
64
  name: "users.show",
60
- params: { id: res.data.id },
65
+ params: { [resource.id]: res.data.id },
61
66
  });
62
67
  })
63
68
  .catch(() => {});
@@ -71,7 +76,7 @@ export default [
71
76
 
72
77
  $router.replace({
73
78
  name: "users.show",
74
- params: { id: res.data.id },
79
+ params: { [resource.id]: res.data.id },
75
80
  });
76
81
  }
77
82
  } else {
@@ -84,7 +89,7 @@ export default [
84
89
 
85
90
  $router.replace({
86
91
  name: "users.show",
87
- params: { id: res.data.id },
92
+ params: { [resource.id]: res.data.id },
88
93
  });
89
94
  }
90
95
  } catch (e) {
@@ -131,9 +136,12 @@ export default [
131
136
  class: "underline",
132
137
  to: {
133
138
  name: "companies.show",
134
- params: { id: model.company_id },
139
+ params: {
140
+ [meta(...companyResource).id]:
141
+ model.company_id,
142
+ },
135
143
  },
136
- text: model.company.name,
144
+ text: model.company?.name,
137
145
  }),
138
146
  },
139
147
  {
@@ -171,7 +179,10 @@ export default [
171
179
  class: "underline",
172
180
  to: {
173
181
  name: "companies.show",
174
- params: { id: model.company_id },
182
+ params: {
183
+ [meta(...companyResource).id]:
184
+ model.company_id,
185
+ },
175
186
  },
176
187
  text: model.company.name,
177
188
  }),
@@ -1,6 +1,4 @@
1
1
  <template>
2
- <VBreadcrumbs :items="breadcrumbs" class="mb-8" container-classes="m-0" />
3
-
4
2
  <div class="container px-6 tablet:px-4 mobile:px-2 mb-8 ml-0 mr-0">
5
3
  <div class="grid__1/1">
6
4
  <div class="grid__1/1 mb-4">
@@ -25,16 +23,10 @@ import Form from "form-backend-validation";
25
23
 
26
24
  export default {
27
25
  components: {
28
- VBreadcrumbs: require("../../../components/layout/Breadcrumbs.vue")
29
- .default,
30
26
  XForm: require("./partials/form.vue").default,
31
27
  },
32
28
 
33
29
  props: {
34
- breadcrumbs: {
35
- type: Array,
36
- required: true,
37
- },
38
30
  resource: {
39
31
  type: Object,
40
32
  required: true,
@@ -60,11 +52,13 @@ export default {
60
52
  await this.resource.form.submit(this);
61
53
  } else {
62
54
  try {
63
- let res = await this.form.post(`${this.resource.api}`);
55
+ let res = await this.form.post(
56
+ `${this.resource.api.endpoint(this)}`,
57
+ );
64
58
 
65
59
  this.$router.replace({
66
60
  name: `${this.resource.slug}.show`,
67
- params: { id: res.data.id },
61
+ params: { [this.resource.id]: res.data.id },
68
62
  });
69
63
  } catch (e) {
70
64
  console.log(e);
@@ -1,10 +1,4 @@
1
1
  <template>
2
- <VBreadcrumbs
3
- :items="addBreadcrumbs"
4
- class="mb-8"
5
- container-classes="m-0"
6
- />
7
-
8
2
  <div class="container px-6 tablet:px-4 mobile:px-2 mb-8 ml-0 mr-0">
9
3
  <div class="grid__1/1">
10
4
  <div class="grid__1/1 mb-4">
@@ -30,16 +24,10 @@ import Form from "form-backend-validation";
30
24
 
31
25
  export default {
32
26
  components: {
33
- VBreadcrumbs: require("../../../components/layout/Breadcrumbs.vue")
34
- .default,
35
27
  XForm: require("./partials/form.vue").default,
36
28
  },
37
29
 
38
30
  props: {
39
- breadcrumbs: {
40
- type: Array,
41
- required: true,
42
- },
43
31
  resource: {
44
32
  type: Object,
45
33
  required: true,
@@ -50,7 +38,6 @@ export default {
50
38
  return {
51
39
  form: null,
52
40
  model: null,
53
- addBreadcrumbs: [...this.$props.breadcrumbs],
54
41
  method: "patch",
55
42
  };
56
43
  },
@@ -64,7 +51,10 @@ export default {
64
51
  async mounted() {
65
52
  window.axios
66
53
  .get(
67
- `${this.resource.api}/${this.$route.params.id}?${this.resource.defaults}`,
54
+ `${this.resource.api.endpoint(this)}/${this.$route.params[this.resource.id]}`,
55
+ {
56
+ params: this.resource.api.params.show,
57
+ },
68
58
  )
69
59
  .then((res) => {
70
60
  this.model = res.data.data;
@@ -75,14 +65,6 @@ export default {
75
65
  this.form[key] = value;
76
66
  },
77
67
  );
78
-
79
- this.addBreadcrumbs.push({
80
- href: {
81
- name: `${this.resource.slug}.show`,
82
- param: this.model.id,
83
- },
84
- text: this.model.name,
85
- });
86
68
  });
87
69
  },
88
70
 
@@ -93,12 +75,12 @@ export default {
93
75
  } else {
94
76
  try {
95
77
  let res = await this.form.patch(
96
- `${this.resource.api}/${this.model.id}`,
78
+ `${this.resource.api.endpoint(this)}/${this.model.id}`,
97
79
  );
98
80
 
99
81
  this.$router.replace({
100
82
  name: `${this.resource.slug}.show`,
101
- params: { id: res.data.id },
83
+ params: { [this.resource.id]: res.data.id },
102
84
  });
103
85
  } catch (e) {
104
86
  console.log(e);
@@ -1,12 +1,6 @@
1
1
  <template>
2
- <VBreadcrumbs :items="breadcrumbs" />
3
-
4
2
  <div class="container px-6 tablet:px-4 mobile:px-2 mb-8 ml-0 mr-0">
5
3
  <div class="grid__1/1">
6
- <h2 class="h1 pb-4">
7
- {{ breadcrumbs[breadcrumbs.length - 1].text }}
8
- </h2>
9
-
10
4
  <div class="flex gap items-center justify-end">
11
5
  <template
12
6
  v-for="(render, index) in resource.index.actions"
@@ -28,20 +22,15 @@
28
22
 
29
23
  <script>
30
24
  export default {
31
- components: {
32
- VBreadcrumbs: require("../../../components/layout/Breadcrumbs.vue")
33
- .default,
34
- },
35
-
36
25
  props: {
37
- breadcrumbs: {
38
- type: Array,
39
- required: true,
40
- },
41
26
  resource: {
42
27
  type: Object,
43
28
  required: true,
44
29
  },
30
+ depth: {
31
+ type: Number,
32
+ required: true,
33
+ },
45
34
  },
46
35
  };
47
36
  </script>
@@ -1,47 +1,44 @@
1
1
  <template>
2
- <VBreadcrumbs
3
- :items="addBreadcrumbs"
4
- class="mb-8"
5
- container-classes="m-0"
6
- />
2
+ <!-- Render nested router view if this isn't the deepest route -->
3
+ <router-view v-if="!deepestRoute" v-slot="{ Component }">
4
+ <component :is="Component" :depth="depth + 1" />
5
+ </router-view>
7
6
 
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
- >
7
+ <template v-else>
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.id} ${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
+ </VelPageHeader>
23
+ <hr class="my-3 hr-muted" />
16
24
  <template
17
- v-for="(render, index) in resource.show.actions"
25
+ v-for="(render, index) in resource.show.layout"
18
26
  :key="index"
19
27
  >
20
28
  <component :is="render(this)" />
29
+ <hr
30
+ v-if="index < resource.show.layout.length - 1"
31
+ class="my-3 hr-muted"
32
+ />
21
33
  </template>
22
- </VelPageHeader>
23
-
24
- <hr class="my-3 hr-muted" />
25
-
26
- <template
27
- v-for="(render, index) in resource.show.layout"
28
- :key="index"
29
- >
30
- <component :is="render(this)" />
31
-
32
- <hr
33
- v-if="index < resource.show.layout.length - 1"
34
- class="my-3 hr-muted"
35
- />
36
- </template>
34
+ </div>
35
+ </template>
36
+ <div v-else class="absolute transform-center text-center">
37
+ <VelSpinner />
37
38
  </div>
38
- </template>
39
-
40
- <div v-else class="absolute transform-center text-center">
41
- <VelSpinner />
42
39
  </div>
43
40
  </div>
44
- </div>
41
+ </template>
45
42
  </template>
46
43
 
47
44
  <script>
@@ -51,8 +48,6 @@ import VelButton from "../../../components/basic/Button.vue";
51
48
 
52
49
  export default {
53
50
  components: {
54
- VBreadcrumbs: require("../../../components/layout/Breadcrumbs.vue")
55
- .default,
56
51
  VelPageHeader: require("../../../components/layout/PageHeader.vue")
57
52
  .default,
58
53
  VelSpinner,
@@ -68,29 +63,39 @@ export default {
68
63
  type: Object,
69
64
  required: true,
70
65
  },
66
+ depth: {
67
+ type: Number,
68
+ required: true,
69
+ },
71
70
  },
72
71
 
73
72
  data() {
74
73
  return {
75
74
  model: null,
76
- addBreadcrumbs: [...this.$props.breadcrumbs],
77
75
  };
78
76
  },
79
77
 
78
+ computed: {
79
+ // This boolean helps determine if we are the final depth of route being rendered
80
+ deepestRoute() {
81
+ return this.depth === this.$route.matched.length;
82
+ },
83
+ },
84
+
80
85
  mounted() {
86
+ if (!this.deepestRoute) {
87
+ return;
88
+ }
89
+
81
90
  axios
82
91
  .get(
83
- `${this.resource.api}/${this.$route.params.id}?${this.resource.defaults}`,
92
+ `${this.resource.api.endpoint(this)}/${this.$route.params[`${this.resource.slug}Id`]}`,
93
+ {
94
+ params: this.resource.api.params.show,
95
+ },
84
96
  )
85
97
  .then((res) => {
86
98
  this.model = res.data.data;
87
- this.addBreadcrumbs.push({
88
- href: {
89
- name: `${this.resource.slug}.show`,
90
- param: this.model.id,
91
- },
92
- text: this.model.name,
93
- });
94
99
  });
95
100
  },
96
101
  };
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
 
3
- import { merge, kebabCase } from "lodash";
3
+ import { merge, kebabCase, cloneDeepWith } from "lodash";
4
4
  import axios from "axios";
5
5
  import { h, resolveComponent } from "vue";
6
6
 
7
- import VTableSorter from "../../components/layout/TableSorter.vue";
7
+ import VelTableSorter from "../../components/layout/TableSorter.vue";
8
8
  import { ElDescriptions, ElDescriptionsItem, ElPopconfirm } from "element-plus";
9
9
  import VelButton from "../../components/basic/Button.vue";
10
10
 
@@ -12,6 +12,7 @@ export const defaultResource = meta();
12
12
 
13
13
  export function meta(name = "default", properties = {}) {
14
14
  const singular = properties.singular || name.slice(0, -1);
15
+ const slug = properties.slug || kebabCase(name);
15
16
 
16
17
  return merge(
17
18
  {
@@ -20,8 +21,16 @@ export function meta(name = "default", properties = {}) {
20
21
  singular,
21
22
  label: singular,
22
23
  multiLabel: name,
23
- slug: properties?.slug || kebabCase(name),
24
- api: `/api/${properties?.slug || kebabCase(name)}`,
24
+ slug,
25
+ id: properties.id || `${slug}Id`,
26
+ icon: `icon-${singular}`,
27
+ api: {
28
+ endpoint: () => `/api/${properties.slug || kebabCase(name)}`,
29
+ params: {
30
+ index: {},
31
+ show: {},
32
+ },
33
+ },
25
34
  permissions: {
26
35
  create: () => true,
27
36
  edit: () => true,
@@ -37,6 +46,82 @@ export function meta(name = "default", properties = {}) {
37
46
  structure: [],
38
47
  },
39
48
  table: {
49
+ actions: [
50
+ ({ $router, resource, model }) =>
51
+ h(
52
+ VelButton,
53
+ {
54
+ tag: "a",
55
+ size: "small",
56
+ type: "primary",
57
+ onClick: () => {
58
+ $router.push({
59
+ name: `${resource.slug}.show`,
60
+ params: {
61
+ [resource.id]: model.id,
62
+ },
63
+ });
64
+ },
65
+ },
66
+ () => "View",
67
+ ),
68
+ (props) => {
69
+ const { $router, resource, model } = props;
70
+
71
+ if (resource.permissions.edit(props)) {
72
+ return h(
73
+ VelButton,
74
+ {
75
+ tag: "a",
76
+ size: "small",
77
+ onClick: () => {
78
+ $router.push({
79
+ name: `${resource.slug}.edit`,
80
+ params: {
81
+ [resource.id]: model.id,
82
+ },
83
+ });
84
+ },
85
+ },
86
+ () => "Edit",
87
+ );
88
+ }
89
+ },
90
+ (props) => {
91
+ const { resource, model, $emit } = props;
92
+
93
+ if (resource.permissions.delete(props)) {
94
+ return h(
95
+ ElPopconfirm,
96
+ {
97
+ title: `Are you sure you want to delete this ${resource.singular}?`,
98
+ confirmButtonText: "Delete",
99
+ cancelButtonText: "Cancel",
100
+ confirmButtonType: "danger",
101
+ onConfirm: async () => {
102
+ await axios.delete(
103
+ `${resource.api.endpoint(props)}/${model.id}`,
104
+ );
105
+
106
+ $emit("reload");
107
+ },
108
+ },
109
+ {
110
+ reference: () =>
111
+ h(
112
+ VelButton,
113
+ {
114
+ tag: "a",
115
+ type: "danger",
116
+ size: "small",
117
+ },
118
+ () => `Delete`,
119
+ ),
120
+ },
121
+ );
122
+ }
123
+ },
124
+ ],
40
125
  structure: [
41
126
  {
42
127
  key: "id",
@@ -51,24 +136,88 @@ export function meta(name = "default", properties = {}) {
51
136
  ],
52
137
  },
53
138
  index: {
54
- actions: [],
139
+ structure: (props) => {
140
+ const { resource, $router, $route, $store, ...rest } =
141
+ props;
142
+
143
+ return {
144
+ key: "PIndex",
145
+ "json-data": {
146
+ ...resource,
147
+ tableStructure: resource.table.structure.concat(
148
+ resource.table.actions.length
149
+ ? [
150
+ {
151
+ key: "actions",
152
+ render: ({ model, $emit }) =>
153
+ h(
154
+ "div",
155
+ {
156
+ class: "flex gap-2",
157
+ },
158
+ resource.table.actions.map(
159
+ (d) =>
160
+ d({
161
+ resource,
162
+ $router,
163
+ $route,
164
+ $store,
165
+ ...rest,
166
+ model,
167
+ $emit,
168
+ }),
169
+ ),
170
+ ),
171
+ },
172
+ ]
173
+ : [],
174
+ ),
175
+ api: resource.api.endpoint(props),
176
+ },
177
+ apiParams: resource.api.params.index,
178
+ idKey: resource.id,
179
+ "fixed-height": false,
180
+ displayActions: false,
181
+ };
182
+ },
183
+ actions: [
184
+ (props) => {
185
+ const { resource, $router } = props;
186
+
187
+ if (resource.permissions.create(props)) {
188
+ return h(
189
+ VelButton,
190
+ {
191
+ tag: "a",
192
+ type: "primary",
193
+ size: "large",
194
+ onClick: () => {
195
+ $router.push({
196
+ name: `${resource.slug}.create`,
197
+ });
198
+ },
199
+ },
200
+ () => [
201
+ h(resolveComponent("GIcon"), {
202
+ class: "fill-0 mr-0.5 icon--0.5",
203
+ name: "icon-plus",
204
+ embed: true,
205
+ artboard: true,
206
+ }),
207
+ `Create ${resource.singular}`,
208
+ ],
209
+ );
210
+ }
211
+ },
212
+ ],
55
213
  layout: [
56
214
  (props) => {
57
215
  const { resource } = props;
58
216
 
59
- return h(VTableSorter, {
60
- key: "PIndex",
61
- "json-data": {
62
- ...resource,
63
- tableStructure: resource.table.structure,
64
- },
65
- defaults: resource.defaults,
66
- "fixed-height": false,
67
- "display-create-action":
68
- resource.permissions.create(props),
69
- "display-edit-action":
70
- resource.permissions.edit(props),
71
- });
217
+ return h(
218
+ VelTableSorter,
219
+ resource.index.structure(props),
220
+ );
72
221
  },
73
222
  ],
74
223
  },
@@ -86,7 +235,9 @@ export function meta(name = "default", properties = {}) {
86
235
  onClick: () => {
87
236
  $router.push({
88
237
  name: `${resource.slug}.edit`,
89
- params: { id: model.id },
238
+ params: {
239
+ [resource.id]: model.id,
240
+ },
90
241
  });
91
242
  },
92
243
  },
@@ -115,7 +266,7 @@ export function meta(name = "default", properties = {}) {
115
266
  confirmButtonType: "danger",
116
267
  onConfirm: async () => {
117
268
  await axios.delete(
118
- `${resource.api}/${model.id}`,
269
+ `${resource.api.endpoint(props)}/${model.id}`,
119
270
  );
120
271
 
121
272
  $router.push({
@@ -182,8 +333,6 @@ export function meta(name = "default", properties = {}) {
182
333
  },
183
334
  ],
184
335
  },
185
- defaults: "",
186
- icon: `icon-${singular}`,
187
336
  },
188
337
  properties,
189
338
  );
@@ -228,18 +377,25 @@ export function columns(columns = []) {
228
377
  }
229
378
 
230
379
  // Export resource
231
- export function routes(node, name, properties = {}) {
380
+ export function routes(
381
+ node,
382
+ name,
383
+ properties = {},
384
+ children = [],
385
+ isChild = false,
386
+ ) {
232
387
  const resource = meta(name, properties);
233
388
 
234
389
  return [
235
390
  {
236
- path: `/${resource.slug}`,
391
+ path: `${isChild ? "" : "/"}${resource.slug}`,
237
392
  component: node ? "" : require("../resource/parent.vue").default,
238
393
  name,
239
394
  meta: {
240
395
  resource,
241
396
  title: resource.title,
242
397
  icon: resource.icon,
398
+ breadcrumb: () => resource.title,
243
399
  },
244
400
  children: [
245
401
  {
@@ -257,18 +413,34 @@ export function routes(node, name, properties = {}) {
257
413
  name: `${resource.slug}.create`,
258
414
  },
259
415
  {
260
- path: ":id",
416
+ path: `:${resource.id}`,
261
417
  component: node
262
418
  ? ""
263
419
  : require("../resource/Children/show.vue").default,
264
420
  name: `${resource.slug}.show`,
421
+ // Remove leading / for nested routes or they'll resolve to the root of the site
422
+ children: cloneDeepWith(children, (value, key) => {
423
+ if (
424
+ key === "path" &&
425
+ typeof value === "string" &&
426
+ value.startsWith("/")
427
+ ) {
428
+ return value.slice(1);
429
+ }
430
+ }),
431
+ meta: {
432
+ breadcrumb: ({ $route }) => $route.params[resource.id],
433
+ },
265
434
  },
266
435
  {
267
- path: ":id/edit",
436
+ path: `:${resource.id}/edit`,
268
437
  component: node
269
438
  ? ""
270
439
  : require("../resource/Children/edit.vue").default,
271
440
  name: `${resource.slug}.edit`,
441
+ meta: {
442
+ breadcrumb: ({ $route }) => $route.params[resource.id],
443
+ },
272
444
  },
273
445
  ],
274
446
  },
@@ -1,19 +1,39 @@
1
1
  <template>
2
- <PageTitle :title="resource.title" />
2
+ <!-- Only render title once at the shallowest route -->
3
+ <template v-if="depth === 1">
4
+ <VelPageTitle :title="resource.title" />
5
+ <VelBreadcrumbs
6
+ :items="breadcrumbs"
7
+ class="mb-8"
8
+ container-classes="m-0"
9
+ />
10
+ </template>
3
11
 
4
12
  <router-view
5
13
  :key="$route.path"
14
+ v-slot="{ Component }"
6
15
  :breadcrumbs="breadcrumbs"
7
16
  :resource="resource"
8
- />
17
+ >
18
+ <component :is="Component" :depth="depth + 1" />
19
+ </router-view>
9
20
  </template>
10
21
 
11
22
  <script>
12
- import PageTitle from "../../components/layout/pageTitle.vue";
23
+ import VelPageTitle from "../../components/layout/pageTitle.vue";
24
+ import VelBreadcrumbs from "../../components/layout/Breadcrumbs.vue";
13
25
 
14
26
  export default {
15
27
  components: {
16
- PageTitle,
28
+ VelPageTitle,
29
+ VelBreadcrumbs,
30
+ },
31
+
32
+ props: {
33
+ depth: {
34
+ type: Number,
35
+ default: 1,
36
+ },
17
37
  },
18
38
 
19
39
  computed: {
@@ -23,18 +43,20 @@ export default {
23
43
  breadcrumbs() {
24
44
  return [
25
45
  {
26
- href: {
27
- name: "index",
28
- },
29
46
  text: "Home",
47
+ href: { name: "index" },
30
48
  },
31
- {
32
- href: {
33
- name: `${this.resource.slug}.index`,
34
- },
35
- text: this.resource.title,
36
- },
37
- ];
49
+ ].concat(
50
+ this.$route.matched
51
+ .filter((route) => route.meta?.breadcrumb)
52
+ .map((route) => ({
53
+ text: route.meta.breadcrumb(this),
54
+ href: {
55
+ name: route.name,
56
+ params: this.$route.params,
57
+ },
58
+ })),
59
+ );
38
60
  },
39
61
  },
40
62
  };
package/index.js CHANGED
@@ -26,6 +26,8 @@ export { default as Wysiwyg2 } from "./_Build/vue/components/form/Wysiwyg2.vue";
26
26
  export { default as Upload } from "./_Build/vue/components/form/Upload.vue";
27
27
  export { default as InputNumber } from "./_Build/vue/components/form/InputNumber.vue";
28
28
 
29
+ export { default as RoleLegend } from "./_Build/vue/components/layout/RoleLegend.vue";
30
+ export { default as TableSorter } from "./_Build/vue/components/layout/TableSorter.vue";
29
31
  export { default as Chip } from "./_Build/vue/components/layout/Chip.vue";
30
32
  export { default as Chips } from "./_Build/vue/components/layout/Chips.vue";
31
33
  export { default as SideBar } from "./_Build/vue/components/layout/SideBar.vue";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishawack/lab-velocity",
3
- "version": "2.0.0-beta.23",
3
+ "version": "2.0.0-beta.25",
4
4
  "description": "Avalere Health branded style system",
5
5
  "scripts": {
6
6
  "setup": "npm ci || npm i && npm run content",