@fishawack/lab-velocity 2.0.0-beta.3 → 2.0.0-beta.31

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 (108) hide show
  1. package/README.md +441 -37
  2. package/_Build/vue/components/basic/Button.vue +1 -1
  3. package/_Build/vue/components/form/Checkbox.vue +10 -0
  4. package/_Build/vue/components/form/Select.vue +231 -28
  5. package/_Build/vue/components/form/Spinner.vue +5 -0
  6. package/_Build/vue/components/layout/Alert.vue +5 -5
  7. package/_Build/vue/components/layout/Audit.vue +75 -0
  8. package/_Build/vue/{modules/AuthModule/components/VBreadcrumbs.vue → components/layout/Breadcrumbs.vue} +4 -4
  9. package/_Build/vue/{modules/AuthModule/components → components/layout}/Chips.vue +2 -2
  10. package/_Build/vue/components/layout/Footer.vue +11 -10
  11. package/_Build/vue/{modules/AuthModule/components/VFormFooter.vue → components/layout/FormFooter.vue} +13 -7
  12. package/_Build/vue/{modules/AuthModule/components → components/layout}/FormRole.vue +10 -8
  13. package/_Build/vue/components/layout/Layout.vue +76 -0
  14. package/_Build/vue/components/layout/Navigation.vue +77 -0
  15. package/_Build/vue/{modules/AuthModule/components/VPageHeader.vue → components/layout/PageHeader.vue} +7 -2
  16. package/_Build/vue/components/layout/SideBar.vue +26 -0
  17. package/_Build/vue/{modules/AuthModule/components/VTable.vue → components/layout/Table.vue} +32 -16
  18. package/_Build/vue/{modules/AuthModule/components/VTableSorter.vue → components/layout/TableSorter.vue} +68 -43
  19. package/_Build/vue/components/layout/pageTitle.vue +1 -1
  20. package/_Build/vue/components/navigation/MenuItem.vue +7 -2
  21. package/_Build/vue/components/navigation/MenuItemGroup.vue +7 -2
  22. package/_Build/vue/modules/AuthModule/js/axios.js +19 -0
  23. package/_Build/vue/modules/AuthModule/js/router.js +28 -88
  24. package/_Build/vue/modules/AuthModule/js/store.js +15 -6
  25. package/_Build/vue/modules/AuthModule/{adminRoutes/PCompanies/Children/partials → routes/PCompanies}/form.vue +50 -18
  26. package/_Build/vue/modules/AuthModule/routes/PCompanies/resource.js +259 -0
  27. package/_Build/vue/modules/AuthModule/routes/PTeams/resource.js +308 -0
  28. package/_Build/vue/modules/AuthModule/{adminRoutes/PUsers/Children/partials → routes/PUsers}/form.vue +30 -18
  29. package/_Build/vue/modules/AuthModule/routes/PUsers/resource.js +215 -0
  30. package/_Build/vue/modules/AuthModule/routes/account-exists.vue +2 -2
  31. package/_Build/vue/modules/AuthModule/routes/change-password.vue +23 -24
  32. package/_Build/vue/modules/AuthModule/routes/container.vue +2 -11
  33. package/_Build/vue/modules/AuthModule/routes/expired-reset.vue +4 -4
  34. package/_Build/vue/modules/AuthModule/routes/expired-verification.vue +9 -8
  35. package/_Build/vue/modules/AuthModule/routes/force-reset.vue +24 -28
  36. package/_Build/vue/modules/AuthModule/routes/forgot.vue +4 -4
  37. package/_Build/vue/modules/AuthModule/routes/login.vue +7 -11
  38. package/_Build/vue/modules/AuthModule/routes/logincallback.vue +2 -4
  39. package/_Build/vue/modules/AuthModule/routes/loginsso.vue +7 -9
  40. package/_Build/vue/modules/AuthModule/routes/logout.vue +1 -3
  41. package/_Build/vue/modules/AuthModule/routes/logoutheadless.vue +1 -3
  42. package/_Build/vue/modules/AuthModule/routes/register.vue +19 -21
  43. package/_Build/vue/modules/AuthModule/routes/reset.vue +14 -13
  44. package/_Build/vue/modules/AuthModule/routes/success-forgot.vue +8 -7
  45. package/_Build/vue/modules/AuthModule/routes/success-reset.vue +2 -2
  46. package/_Build/vue/modules/AuthModule/routes/success-verify.vue +1 -3
  47. package/_Build/vue/modules/AuthModule/routes/verify.vue +11 -14
  48. package/_Build/vue/modules/resource/Children/create.vue +81 -0
  49. package/_Build/vue/modules/resource/Children/edit.vue +106 -0
  50. package/_Build/vue/modules/resource/Children/index.vue +42 -0
  51. package/_Build/vue/modules/resource/Children/partials/form.vue +59 -0
  52. package/_Build/vue/modules/resource/Children/show.vue +140 -0
  53. package/_Build/vue/modules/resource/index.js +494 -0
  54. package/_Build/vue/modules/resource/parent.vue +63 -0
  55. package/_base.scss +0 -1
  56. package/_defaults.scss +2 -13
  57. package/_variables.scss +9 -4
  58. package/components/_alert.scss +5 -0
  59. package/components/_auth.scss +163 -0
  60. package/components/_basic.scss +55 -0
  61. package/components/_breadcrumbs.scss +39 -0
  62. package/components/_button.scss +304 -0
  63. package/components/_cascader.scss +12 -0
  64. package/components/_checkbox.scss +41 -0
  65. package/components/_chip.scss +24 -0
  66. package/components/_collapse.scss +24 -0
  67. package/components/_datepicker.scss +53 -0
  68. package/components/_descriptions.scss +2 -0
  69. package/components/_footer.scss +47 -0
  70. package/components/_form.scss +24 -0
  71. package/components/_header.scss +30 -0
  72. package/components/_icon.scss +25 -0
  73. package/components/_inputNumber.scss +22 -0
  74. package/components/_layout.scss +56 -0
  75. package/components/_link.scss +44 -0
  76. package/components/_loader.scss +43 -0
  77. package/components/_menu.scss +112 -0
  78. package/components/_modal.scss +24 -0
  79. package/components/_pageTitle.scss +8 -0
  80. package/components/_permissionLegend.scss +18 -0
  81. package/components/_select.scss +29 -0
  82. package/components/_sidebar.scss +41 -0
  83. package/components/_switch.scss +14 -0
  84. package/components/_table.scss +20 -0
  85. package/components/_tooltip.scss +4 -0
  86. package/components/_typography.scss +162 -0
  87. package/components/_upload.scss +15 -0
  88. package/components/_wysiwyg.scss +7 -0
  89. package/components/_wysiwyg2.scss +142 -0
  90. package/index.js +10 -1
  91. package/package.json +5 -3
  92. package/vendor.scss +0 -1
  93. package/_Build/vue/components/layout/sideBar.vue +0 -25
  94. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/Children/Upload/upload.vue +0 -251
  95. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/Children/create.vue +0 -62
  96. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/Children/edit.vue +0 -98
  97. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/Children/index.vue +0 -90
  98. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/Children/show.vue +0 -262
  99. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/parent.vue +0 -36
  100. package/_Build/vue/modules/AuthModule/adminRoutes/PUsers/Children/create.vue +0 -112
  101. package/_Build/vue/modules/AuthModule/adminRoutes/PUsers/Children/edit.vue +0 -103
  102. package/_Build/vue/modules/AuthModule/adminRoutes/PUsers/Children/index.vue +0 -112
  103. package/_Build/vue/modules/AuthModule/adminRoutes/PUsers/Children/show.vue +0 -120
  104. package/_Build/vue/modules/AuthModule/adminRoutes/PUsers/parent.vue +0 -36
  105. /package/_Build/vue/{modules/AuthModule/components → components/layout}/AuthModal.vue +0 -0
  106. /package/_Build/vue/{modules/AuthModule/components → components/layout}/Chip.vue +0 -0
  107. /package/_Build/vue/{modules/AuthModule/components/VPasswordValidation.vue → components/layout/PasswordValidation.vue} +0 -0
  108. /package/_Build/vue/{modules/AuthModule/components/VRoleLegend.vue → components/layout/RoleLegend.vue} +0 -0
@@ -0,0 +1,140 @@
1
+ <template>
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>
6
+
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="
15
+ resource.modelTitle
16
+ ? resource.modelTitle(this)
17
+ : `${model.name ?? model.id} ${model.last_name ?? ''}`
18
+ "
19
+ >
20
+ <template
21
+ v-for="(rendered, index) in renderedActions"
22
+ :key="index"
23
+ >
24
+ <component :is="rendered" />
25
+ </template>
26
+ </VelPageHeader>
27
+
28
+ <hr class="my-3 hr-muted" />
29
+
30
+ <el-tabs v-model="active" type="card">
31
+ <template
32
+ v-for="(rendered, index) in renderedLayout"
33
+ :key="index"
34
+ >
35
+ <el-tab-pane :name="index">
36
+ <template #label>
37
+ <span class="align-middle-dive">
38
+ <GIcon
39
+ v-if="rendered.icon"
40
+ class="icon icon--text mr"
41
+ :name="rendered.icon"
42
+ />
43
+ <span>{{
44
+ rendered.label ||
45
+ `Tab ${index + 1}`
46
+ }}</span>
47
+ </span>
48
+ </template>
49
+ <component
50
+ :is="rendered.component || rendered"
51
+ />
52
+ </el-tab-pane>
53
+ </template>
54
+ </el-tabs>
55
+ </div>
56
+ </template>
57
+ <div v-else class="absolute transform-center text-center">
58
+ <VelSpinner />
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </template>
63
+ </template>
64
+
65
+ <script>
66
+ import axios from "axios";
67
+ import VelSpinner from "../../../components/form/Spinner.vue";
68
+ import VelButton from "../../../components/basic/Button.vue";
69
+ import { ElTabs, ElTabPane } from "element-plus";
70
+
71
+ export default {
72
+ components: {
73
+ VelPageHeader: require("../../../components/layout/PageHeader.vue")
74
+ .default,
75
+ VelSpinner,
76
+ VelButton,
77
+ ElTabs,
78
+ ElTabPane,
79
+ },
80
+
81
+ props: {
82
+ breadcrumbs: {
83
+ type: Array,
84
+ required: true,
85
+ },
86
+ resource: {
87
+ type: Object,
88
+ required: true,
89
+ },
90
+ depth: {
91
+ type: Number,
92
+ required: true,
93
+ },
94
+ },
95
+
96
+ data() {
97
+ return {
98
+ model: null,
99
+ active: 0,
100
+ };
101
+ },
102
+
103
+ computed: {
104
+ // This boolean helps determine if we are the final depth of route being rendered
105
+ deepestRoute() {
106
+ return (
107
+ this.depth ===
108
+ this.$route.matched.filter((d) => d.components).length
109
+ );
110
+ },
111
+
112
+ // Compute rendered layout once
113
+ renderedLayout() {
114
+ return this.resource.show.layout.map((render) => render(this));
115
+ },
116
+
117
+ // Compute rendered actions once
118
+ renderedActions() {
119
+ return this.resource.show.actions.map((render) => render(this));
120
+ },
121
+ },
122
+
123
+ mounted() {
124
+ if (!this.deepestRoute) {
125
+ return;
126
+ }
127
+
128
+ axios
129
+ .get(
130
+ `${this.resource.api.endpoint(this)}/${this.$route.params[`${this.resource.id}`]}`,
131
+ {
132
+ params: this.resource.api.params.show(this),
133
+ },
134
+ )
135
+ .then((res) => {
136
+ this.model = res.data.data;
137
+ });
138
+ },
139
+ };
140
+ </script>
@@ -0,0 +1,494 @@
1
+ "use strict";
2
+
3
+ import { merge, kebabCase, snakeCase, cloneDeepWith } from "lodash";
4
+ import axios from "axios";
5
+ import { h, resolveComponent } from "vue";
6
+
7
+ import VelTableSorter from "../../components/layout/TableSorter.vue";
8
+ import {
9
+ ElDescriptions,
10
+ ElDescriptionsItem,
11
+ ElPopconfirm,
12
+ ElNotification,
13
+ } from "element-plus";
14
+ import VelButton from "../../components/basic/Button.vue";
15
+
16
+ export const defaultResource = meta();
17
+
18
+ export function meta(name = "default", properties = {}) {
19
+ const singular = properties.singular || name.slice(0, -1);
20
+ const slug = properties.slug || kebabCase(name);
21
+
22
+ return merge(
23
+ {
24
+ name,
25
+ title: properties.title || name[0].toUpperCase() + name.slice(1),
26
+ singular,
27
+ singularTitle:
28
+ properties.singularTitle ||
29
+ singular[0].toUpperCase() + singular.slice(1),
30
+ label: singular,
31
+ multiLabel: name,
32
+ slug,
33
+ path: properties.path || `/${slug}`,
34
+ routeName: properties.routeName || slug,
35
+ id: properties.id || `${snakeCase(slug)}Id`,
36
+ icon: `icon-${singular}`,
37
+ api: {
38
+ endpoint: () => `/api/${properties.slug || kebabCase(name)}`,
39
+ params: {
40
+ index: () => ({}),
41
+ show: () => ({}),
42
+ },
43
+ },
44
+ permissions: {
45
+ create: () => true,
46
+ edit: () => true,
47
+ delete: () => false,
48
+ },
49
+ searchable: {
50
+ value: "name",
51
+ label: `Search ${name}`,
52
+ },
53
+ form: {
54
+ component: null,
55
+ fields: () => ({}),
56
+ structure: [],
57
+ },
58
+ table: {
59
+ actions: [
60
+ ({ model, resource }, { $router }) =>
61
+ h(
62
+ VelButton,
63
+ {
64
+ tag: "a",
65
+ size: "small",
66
+ type: "primary",
67
+ onClick: () => {
68
+ $router.push({
69
+ name: `${resource.routeName}.show`,
70
+ params: {
71
+ [resource.id]: model.id,
72
+ },
73
+ });
74
+ },
75
+ },
76
+ () => "View",
77
+ ),
78
+ ({ model, resource }, props) => {
79
+ const { $router } = props;
80
+
81
+ if (resource.permissions.edit(props, { model })) {
82
+ return h(
83
+ VelButton,
84
+ {
85
+ tag: "a",
86
+ size: "small",
87
+ onClick: () => {
88
+ $router.push({
89
+ name: `${resource.routeName}.edit`,
90
+ params: {
91
+ [resource.id]: model.id,
92
+ },
93
+ });
94
+ },
95
+ },
96
+ () => "Edit",
97
+ );
98
+ }
99
+ },
100
+ ({ model, resource }, props) => {
101
+ const { $emit } = props;
102
+
103
+ if (resource.permissions.delete(props, { model })) {
104
+ return h({
105
+ data: () => ({
106
+ loading: false,
107
+ }),
108
+ render() {
109
+ return h(
110
+ ElPopconfirm,
111
+ {
112
+ title: `Are you sure you want to delete this ${resource.singular}?`,
113
+ confirmButtonText: "Delete",
114
+ cancelButtonText: "Cancel",
115
+ confirmButtonType: "danger",
116
+ onConfirm: async () => {
117
+ this.loading = true;
118
+
119
+ await axios.delete(
120
+ `${resource.api.endpoint(props)}/${model.id}`,
121
+ );
122
+
123
+ $emit("reload");
124
+
125
+ ElNotification({
126
+ title: "Success",
127
+ message: `${resource.singularTitle} with id ${model.id} deleted.`,
128
+ type: "success",
129
+ });
130
+
131
+ this.loading = false;
132
+ },
133
+ },
134
+ {
135
+ reference: () =>
136
+ h(
137
+ VelButton,
138
+ {
139
+ tag: "a",
140
+ type: "danger",
141
+ size: "small",
142
+ loading: this.loading,
143
+ },
144
+ () => `Delete`,
145
+ ),
146
+ },
147
+ );
148
+ },
149
+ });
150
+ }
151
+ },
152
+ ],
153
+ structure: [
154
+ {
155
+ key: "id",
156
+ },
157
+ ],
158
+ },
159
+ description: {
160
+ structure: [
161
+ {
162
+ key: "id",
163
+ },
164
+ ],
165
+ },
166
+ index: {
167
+ structure: (props) => {
168
+ const { resource } = props;
169
+
170
+ return {
171
+ key: "PIndex",
172
+ "json-data": {
173
+ ...resource,
174
+ tableStructure: resource.table.structure.concat(
175
+ resource.table.actions.length
176
+ ? [
177
+ {
178
+ key: "actions",
179
+ render: ({ model }, props) =>
180
+ h(
181
+ "div",
182
+ {
183
+ class: "flex gap-2",
184
+ },
185
+ resource.table.actions.map(
186
+ (d) =>
187
+ d(
188
+ {
189
+ model,
190
+ resource,
191
+ },
192
+ props,
193
+ ),
194
+ ),
195
+ ),
196
+ },
197
+ ]
198
+ : [],
199
+ ),
200
+ api: resource.api.endpoint(props),
201
+ },
202
+ apiParams: resource.api.params.index(props),
203
+ idKey: resource.id,
204
+ "fixed-height": false,
205
+ displayActions: false,
206
+ };
207
+ },
208
+ actions: [
209
+ (props) => {
210
+ const { resource, $router } = props;
211
+
212
+ if (resource.permissions.create(props)) {
213
+ return h(
214
+ VelButton,
215
+ {
216
+ tag: "a",
217
+ type: "primary",
218
+ size: "large",
219
+ onClick: () => {
220
+ $router.push({
221
+ name: `${resource.routeName}.create`,
222
+ });
223
+ },
224
+ },
225
+ () => [
226
+ h(resolveComponent("GIcon"), {
227
+ class: "fill-0 mr-0.5 icon--0.5",
228
+ name: "icon-plus",
229
+ embed: true,
230
+ artboard: true,
231
+ }),
232
+ `Create ${resource.singular}`,
233
+ ],
234
+ );
235
+ }
236
+ },
237
+ ],
238
+ layout: [
239
+ (props) => {
240
+ const { resource } = props;
241
+
242
+ return h(
243
+ VelTableSorter,
244
+ resource.index.structure(props),
245
+ );
246
+ },
247
+ ],
248
+ },
249
+ show: {
250
+ actions: [
251
+ (props) => {
252
+ const { resource, model, $router } = props;
253
+
254
+ if (resource.permissions.edit(props, { model })) {
255
+ return h(
256
+ VelButton,
257
+ {
258
+ tag: "a",
259
+ type: "primary",
260
+ onClick: () => {
261
+ $router.push({
262
+ name: `${resource.routeName}.edit`,
263
+ params: {
264
+ [resource.id]: model.id,
265
+ },
266
+ });
267
+ },
268
+ },
269
+ () => [
270
+ h(resolveComponent("GIcon"), {
271
+ class: "fill-0 mr-0.5 icon--0.5",
272
+ name: "icon-edit",
273
+ embed: true,
274
+ artboard: true,
275
+ }),
276
+ `Edit ${resource.singular}`,
277
+ ],
278
+ );
279
+ }
280
+ },
281
+ (props) => {
282
+ const { resource, model, $router } = props;
283
+
284
+ if (resource.permissions.delete(props, { model })) {
285
+ return h(
286
+ ElPopconfirm,
287
+ {
288
+ title: `Are you sure you want to delete this ${resource.singular}?`,
289
+ confirmButtonText: "Delete",
290
+ cancelButtonText: "Cancel",
291
+ confirmButtonType: "danger",
292
+ onConfirm: async () => {
293
+ await axios.delete(
294
+ `${resource.api.endpoint(props)}/${model.id}`,
295
+ );
296
+
297
+ $router.push({
298
+ name: `${resource.routeName}.index`,
299
+ });
300
+ },
301
+ },
302
+ {
303
+ reference: () =>
304
+ h(
305
+ VelButton,
306
+ {
307
+ tag: "a",
308
+ type: "danger",
309
+ },
310
+ () => [
311
+ h(resolveComponent("GIcon"), {
312
+ class: "fill-0 mr-0.5 icon--0.5",
313
+ name: "icon-trash",
314
+ embed: true,
315
+ artboard: true,
316
+ }),
317
+ `Delete ${resource.singular}`,
318
+ ],
319
+ ),
320
+ },
321
+ );
322
+ }
323
+ },
324
+ ],
325
+ layout: [
326
+ (props) => {
327
+ const { resource, model } = props;
328
+
329
+ return {
330
+ label: "Details",
331
+ component: h(
332
+ ElDescriptions,
333
+ {
334
+ border: true,
335
+ column: 1,
336
+ },
337
+ () =>
338
+ resource.description.structure.map(
339
+ (item, index) =>
340
+ (!item.condition ||
341
+ item.condition(props)) &&
342
+ h(
343
+ ElDescriptionsItem,
344
+ {
345
+ key: index,
346
+ labelWidth: "20%",
347
+ },
348
+ {
349
+ label: () =>
350
+ item.label ||
351
+ item.key[0].toUpperCase() +
352
+ item.key.slice(1),
353
+ default: () =>
354
+ item.render
355
+ ? h(
356
+ item.render(
357
+ props,
358
+ ),
359
+ )
360
+ : model?.[
361
+ item.key
362
+ ] || "",
363
+ },
364
+ ),
365
+ ),
366
+ ),
367
+ };
368
+ },
369
+ ],
370
+ },
371
+ },
372
+ properties,
373
+ );
374
+ }
375
+
376
+ export function columns(columns = []) {
377
+ return {
378
+ table: {
379
+ structure: columns
380
+ .filter((column) => !column.filter?.table)
381
+ .map((column) => ({
382
+ ...column,
383
+ render: column.render?.read || column.render?.table,
384
+ })),
385
+ },
386
+ description: {
387
+ structure: columns
388
+ .filter((column) => !column.filter?.description)
389
+ .map((column) => ({
390
+ ...column,
391
+ render: column.render?.read || column.render?.description,
392
+ })),
393
+ },
394
+ form: {
395
+ fields: (props) =>
396
+ columns
397
+ .filter((column) => !column.filter?.form)
398
+ .reduce((fields, column) => {
399
+ fields[column.key] = column.initial
400
+ ? column.initial(props)
401
+ : (props.model?.[column.key] ?? null);
402
+ return fields;
403
+ }, {}),
404
+ preparation: (props) =>
405
+ columns
406
+ .filter((column) => !column.filter?.form)
407
+ .reduce((fields, column) => {
408
+ fields[column.key] = column.preparation
409
+ ? column.preparation(props)
410
+ : props.form[column.key];
411
+ return fields;
412
+ }, {}),
413
+ structure: columns
414
+ .filter((column) => !column.filter?.form)
415
+ .map((column) => ({
416
+ ...column,
417
+ render: column.render?.write || column.render?.form,
418
+ })),
419
+ },
420
+ };
421
+ }
422
+
423
+ // Export resource
424
+ export function routes(node, name, properties = {}, children = []) {
425
+ const resource = meta(name, properties);
426
+
427
+ return [
428
+ {
429
+ path: resource.path,
430
+ component: node ? "" : require("../resource/parent.vue").default,
431
+ name: `${resource.routeName}`,
432
+ meta: {
433
+ resource,
434
+ title: resource.title,
435
+ icon: resource.icon,
436
+ breadcrumb: () => resource.title,
437
+ ...properties.meta,
438
+ },
439
+ children: [
440
+ {
441
+ path: "",
442
+ component: node
443
+ ? ""
444
+ : require("../resource/Children/index.vue").default,
445
+ name: `${resource.routeName}.index`,
446
+ },
447
+ {
448
+ path: "create",
449
+ component: node
450
+ ? ""
451
+ : require("../resource/Children/create.vue").default,
452
+ name: `${resource.routeName}.create`,
453
+ },
454
+ {
455
+ path: `:${resource.id}`,
456
+ component: node
457
+ ? ""
458
+ : require("../resource/Children/show.vue").default,
459
+ name: `${resource.routeName}.show`,
460
+ // Remove leading / for nested routes or they'll resolve to the root of the site
461
+ children: cloneDeepWith(children, (value, key) => {
462
+ if (
463
+ key === "path" &&
464
+ typeof value === "string" &&
465
+ value.startsWith("/")
466
+ ) {
467
+ return value.slice(1);
468
+ }
469
+ }),
470
+ meta: {
471
+ breadcrumb: ({ $route }) => $route.params[resource.id],
472
+ },
473
+ },
474
+ {
475
+ path: `:${resource.id}/edit`,
476
+ component: node
477
+ ? ""
478
+ : require("../resource/Children/edit.vue").default,
479
+ name: `${resource.routeName}.edit`,
480
+ meta: {
481
+ breadcrumb: ({ $route }) => $route.params[resource.id],
482
+ },
483
+ },
484
+ ],
485
+ },
486
+ ];
487
+ }
488
+
489
+ export default {
490
+ routes,
491
+ meta,
492
+ columns,
493
+ defaultResource,
494
+ };
@@ -0,0 +1,63 @@
1
+ <template>
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>
11
+
12
+ <router-view
13
+ :key="$route.path"
14
+ v-slot="{ Component }"
15
+ :breadcrumbs="breadcrumbs"
16
+ :resource="resource"
17
+ >
18
+ <component :is="Component" :depth="depth + 1" />
19
+ </router-view>
20
+ </template>
21
+
22
+ <script>
23
+ import VelPageTitle from "../../components/layout/pageTitle.vue";
24
+ import VelBreadcrumbs from "../../components/layout/Breadcrumbs.vue";
25
+
26
+ export default {
27
+ components: {
28
+ VelPageTitle,
29
+ VelBreadcrumbs,
30
+ },
31
+
32
+ props: {
33
+ depth: {
34
+ type: Number,
35
+ default: 1,
36
+ },
37
+ },
38
+
39
+ computed: {
40
+ resource() {
41
+ return this.$route.meta.resource;
42
+ },
43
+ breadcrumbs() {
44
+ return [
45
+ {
46
+ text: "Home",
47
+ href: { name: "index" },
48
+ },
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
+ );
60
+ },
61
+ },
62
+ };
63
+ </script>