@fishawack/lab-velocity 2.0.0-beta.27 → 2.0.0-beta.29

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
@@ -187,6 +187,8 @@ There are two different set of sass imports for the admin and the frontend route
187
187
  @import "@fishawack/lab-velocity/components/sidebar";
188
188
  @import "@fishawack/lab-velocity/components/menu";
189
189
  @import "@fishawack/lab-velocity/components/layout";
190
+ @import "element-plus/theme-chalk/el-tabs";
191
+ @import "element-plus/theme-chalk/el-tab-pane";
190
192
  ```
191
193
 
192
194
  Lastly for the admin layout & navigation import & apply the Layout component to the app.vue within your project.
@@ -91,7 +91,7 @@ export default {
91
91
 
92
92
  computed: {
93
93
  permissions() {
94
- const allPermissions = this.form.roles.reduce((acc, id) => {
94
+ const allPermissions = (this.form.roles || []).reduce((acc, id) => {
95
95
  return acc.concat(
96
96
  this.roles.find((d) => d.id === id)?.permissions || [],
97
97
  );
@@ -9,12 +9,14 @@ import { routes as resourceRoutes } from "../../resource/index.js";
9
9
 
10
10
  import userResource from "../routes/PUsers/resource.js";
11
11
  import companyResource from "../routes/PCompanies/resource.js";
12
+ import teamResource from "../routes/PTeams/resource.js";
12
13
 
13
14
  // Admin routes export - minimal auth flow (headless login only)
14
15
  export function adminRoutes(node, overrides = {}) {
15
16
  const {
16
17
  userResource: overrideUserResource = {},
17
18
  companyResource: overrideCompanyResource = {},
19
+ teamResource: overrideTeamResource = {},
18
20
  } = overrides;
19
21
 
20
22
  return [
@@ -58,6 +60,12 @@ export function adminRoutes(node, overrides = {}) {
58
60
  ...resourceRoutes(
59
61
  node,
60
62
  ...merge(companyResource, [undefined, overrideCompanyResource]),
63
+ [
64
+ ...resourceRoutes(
65
+ node,
66
+ ...merge(teamResource, [undefined, overrideTeamResource]),
67
+ ),
68
+ ],
61
69
  ),
62
70
  ];
63
71
  }
@@ -6,6 +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 teamResource from "../PTeams/resource.js";
9
10
  import { defaultResource, meta } from "../../../resource/index.js";
10
11
 
11
12
  import { ElNotification } from "element-plus";
@@ -17,7 +18,7 @@ export default [
17
18
  {
18
19
  api: {
19
20
  params: {
20
- show: { include: "primary_contact" },
21
+ show: () => ({ include: "primary_contact" }),
21
22
  },
22
23
  },
23
24
  permissions: {
@@ -151,53 +152,106 @@ export default [
151
152
  ],
152
153
  layout: [
153
154
  ...defaultResource.show.layout,
154
- ({ model }) =>
155
- h(VelFormRole, {
155
+ ({ model }) => ({
156
+ label: "Access control",
157
+ component: h(VelFormRole, {
156
158
  overrides: model.overrides_roles_and_permissions,
157
159
  form: { roles: model.roles.map((d) => d.id) },
158
160
  readonly: true,
159
161
  }),
160
- ({ model, $store, $router, ...rest }) => {
162
+ }),
163
+ ({ model, $store, $router, $route, ...rest }) => {
164
+ const resource = meta(...teamResource);
165
+
166
+ const props = {
167
+ model,
168
+ $store,
169
+ $router,
170
+ $route,
171
+ ...rest,
172
+ resource,
173
+ };
174
+
175
+ return {
176
+ label: "Teams",
177
+ component: h("div", [
178
+ h("div", { class: "flex justify-end items-end" }, [
179
+ resource.permissions.create(props) &&
180
+ h(
181
+ VelButton,
182
+ {
183
+ tag: "a",
184
+ type: "primary",
185
+ size: "large",
186
+ onClick: () => {
187
+ $router.push({
188
+ name: `${resource.slug}.create`,
189
+ });
190
+ },
191
+ },
192
+ () => [
193
+ h(resolveComponent("GIcon"), {
194
+ class: "fill-0 mr-0.5 icon--0.5",
195
+ name: "icon-plus",
196
+ embed: true,
197
+ artboard: true,
198
+ }),
199
+ `Create ${resource.singular}`,
200
+ ],
201
+ ),
202
+ ]),
203
+ h(VelTableSorter, resource.index.structure(props)),
204
+ ]),
205
+ };
206
+ },
207
+ ({ model, $store, $router, $route, ...rest }) => {
161
208
  const resource = meta(...userResource);
162
209
 
163
- resource.api.params.index["filter[company_id]"] = model.id;
210
+ resource.api.params.index = ({ $route }) => ({
211
+ include: "company",
212
+ "filter[company_id]": $route.params.companiesId,
213
+ });
164
214
 
165
215
  const props = {
166
216
  model,
167
217
  $store,
168
218
  $router,
219
+ $route,
169
220
  ...rest,
170
221
  resource,
171
222
  };
172
223
 
173
- return h("div", [
174
- h("div", { class: "flex justify-end items-end" }, [
175
- resource.permissions.create(props) &&
176
- h(
177
- VelButton,
178
- {
179
- tag: "a",
180
- type: "primary",
181
- size: "large",
182
- onClick: () => {
183
- $router.push({
184
- name: `${resource.slug}.create`,
185
- });
224
+ return {
225
+ label: "Users",
226
+ component: h("div", [
227
+ h("div", { class: "flex justify-end items-end" }, [
228
+ resource.permissions.create(props) &&
229
+ h(
230
+ VelButton,
231
+ {
232
+ tag: "a",
233
+ type: "primary",
234
+ size: "large",
235
+ onClick: () => {
236
+ $router.push({
237
+ name: `${resource.slug}.create`,
238
+ });
239
+ },
186
240
  },
187
- },
188
- () => [
189
- h(resolveComponent("GIcon"), {
190
- class: "fill-0 mr-0.5 icon--0.5",
191
- name: "icon-plus",
192
- embed: true,
193
- artboard: true,
194
- }),
195
- `Create ${resource.singular}`,
196
- ],
197
- ),
241
+ () => [
242
+ h(resolveComponent("GIcon"), {
243
+ class: "fill-0 mr-0.5 icon--0.5",
244
+ name: "icon-plus",
245
+ embed: true,
246
+ artboard: true,
247
+ }),
248
+ `Create ${resource.singular}`,
249
+ ],
250
+ ),
251
+ ]),
252
+ h(VelTableSorter, resource.index.structure(props)),
198
253
  ]),
199
- h(VelTableSorter, resource.index.structure(props)),
200
- ]);
254
+ };
201
255
  },
202
256
  ],
203
257
  },
@@ -0,0 +1,308 @@
1
+ import { merge } from "lodash";
2
+ import { h, resolveComponent } from "vue";
3
+ import axios from "axios";
4
+ import { throttle } from "lodash";
5
+
6
+ import { columns } from "../../../resource/index.js";
7
+ import userResource from "../PUsers/resource.js";
8
+ import { defaultResource, meta } from "../../../resource/index.js";
9
+
10
+ import VelFormRole from "../../../../components/layout/FormRole.vue";
11
+ import Chip from "../../../../components/layout/Chip.vue";
12
+ import Chips from "../../../../components/layout/Chips.vue";
13
+ import VelRoleLegend from "../../../../components/layout/RoleLegend.vue";
14
+
15
+ import VelTableSorter from "../../../../components/layout/TableSorter.vue";
16
+ import VelButton from "../../../../components/basic/Button.vue";
17
+ import VelCheckbox from "../../../../components/form/Checkbox.vue";
18
+
19
+ export default [
20
+ "teams",
21
+ {
22
+ icon: `icon-account-circle`,
23
+ api: {
24
+ params: {
25
+ index: ({ $route }) => ({
26
+ include: "company",
27
+ "filter[company_id]": $route.params.companiesId,
28
+ }),
29
+ show: () => ({
30
+ include: "company",
31
+ }),
32
+ },
33
+ },
34
+ permissions: {
35
+ create: ({ $store }) => $store.getters.can("write teams"),
36
+ edit: ({ $store }) => $store.getters.can("write teams"),
37
+ delete: ({ $store }) => $store.getters.can("delete teams"),
38
+ },
39
+ ...merge(
40
+ columns([
41
+ {
42
+ key: "name",
43
+ sortable: true,
44
+ },
45
+ {
46
+ key: "description",
47
+ },
48
+ {
49
+ key: "company_id",
50
+ label: "Company",
51
+ class: "hidden",
52
+ render: {
53
+ read: ({ model }) => h("span", model.company.name),
54
+ },
55
+ initial: ({ $route, model }) =>
56
+ model?.company_id || $route.params.companiesId || null,
57
+ },
58
+ {
59
+ key: "roles",
60
+ initial: ({ model }) =>
61
+ model?.roles.map((val) => val.id) || [],
62
+ render: {
63
+ read: ({ model }) =>
64
+ h(
65
+ !model.overrides_roles_and_permissions ||
66
+ model.roles.length === 1
67
+ ? Chip
68
+ : Chips,
69
+ !model.overrides_roles_and_permissions
70
+ ? {
71
+ name: "inherited",
72
+ label: "Inherited",
73
+ }
74
+ : model.roles.length === 1
75
+ ? {
76
+ name: model.roles[0].name,
77
+ label: model.roles[0].label,
78
+ }
79
+ : { array: model.roles },
80
+ ),
81
+ write: ({ model, form }) =>
82
+ h(VelFormRole, {
83
+ overrides:
84
+ model?.overrides_roles_and_permissions,
85
+ form,
86
+ }),
87
+ },
88
+ },
89
+ ]),
90
+ {
91
+ index: {
92
+ layout: [
93
+ ...defaultResource.index.layout,
94
+ () =>
95
+ h(VelRoleLegend, {
96
+ class: "mt-5",
97
+ }),
98
+ ],
99
+ },
100
+ show: {
101
+ layout: [
102
+ ...defaultResource.show.layout,
103
+ ({ model }) => ({
104
+ label: "Access control",
105
+ component: h(VelFormRole, {
106
+ overrides:
107
+ model.overrides_roles_and_permissions,
108
+ form: { roles: model.roles.map((d) => d.id) },
109
+ readonly: true,
110
+ }),
111
+ }),
112
+ (props) => {
113
+ const { model, $store, $router, $route, ...rest } =
114
+ props;
115
+
116
+ return {
117
+ label: "Members",
118
+ component: h({
119
+ data: () => ({
120
+ scoped: true,
121
+ }),
122
+ mounted() {
123
+ this.emitter.on("reload-teams", () => {
124
+ this.reload();
125
+ });
126
+ },
127
+ beforeUnmount() {
128
+ this.emitter.off("reload-teams");
129
+ },
130
+ methods: {
131
+ reload: throttle(function () {
132
+ this.$refs.members.reload();
133
+ this.$refs.users.reload();
134
+ }, 1000),
135
+ },
136
+ render() {
137
+ return h("div", [
138
+ h("h3", "Members"),
139
+ (() => {
140
+ const resource = meta(
141
+ ...userResource,
142
+ );
143
+
144
+ resource.api.params.index = ({
145
+ $route,
146
+ }) => ({
147
+ include: "company",
148
+ "filter[teams.id]":
149
+ $route.params.teamsId,
150
+ });
151
+
152
+ resource.table.actions = [
153
+ ({ model }) =>
154
+ h({
155
+ data: () => ({
156
+ loading: false,
157
+ }),
158
+ render() {
159
+ return h(
160
+ VelButton,
161
+ {
162
+ tag: "a",
163
+ size: "small",
164
+ type: "warning",
165
+ loading:
166
+ this
167
+ .loading,
168
+ onClick:
169
+ async () => {
170
+ this.loading = true;
171
+
172
+ await axios.delete(
173
+ `api/teams/${$route.params.teamsId}/users/${model.id}`,
174
+ );
175
+
176
+ this.emitter.emit(
177
+ "reload-teams",
178
+ );
179
+ },
180
+ },
181
+ "Remove",
182
+ );
183
+ },
184
+ }),
185
+ ];
186
+
187
+ const props = {
188
+ model,
189
+ $store,
190
+ $router,
191
+ $route,
192
+ ...rest,
193
+ resource,
194
+ };
195
+
196
+ return h(
197
+ VelTableSorter,
198
+ merge(
199
+ resource.index.structure(
200
+ props,
201
+ ),
202
+ {
203
+ ref: "members",
204
+ },
205
+ ),
206
+ );
207
+ })(),
208
+ h("h3", "Users"),
209
+ h(VelCheckbox, {
210
+ label: "Only show users from this company",
211
+ class: "mt-2",
212
+ modelValue: this.scoped,
213
+ "onUpdate:modelValue": (
214
+ value,
215
+ ) => {
216
+ this.scoped = value;
217
+ this.$nextTick(() => {
218
+ this.$refs.users.reload();
219
+ });
220
+ },
221
+ }),
222
+ (() => {
223
+ const resource = meta(
224
+ ...userResource,
225
+ );
226
+
227
+ resource.api.params.index = ({
228
+ $route,
229
+ }) => ({
230
+ include: "company",
231
+ "filter[company_id]": this
232
+ .scoped
233
+ ? $route.params
234
+ .companiesId
235
+ : null,
236
+ "filter[teams.id]": `!${$route.params.teamsId}`,
237
+ });
238
+
239
+ resource.table.actions = [
240
+ ({ model }) =>
241
+ h({
242
+ data: () => ({
243
+ loading: false,
244
+ }),
245
+ render() {
246
+ return h(
247
+ VelButton,
248
+ {
249
+ tag: "a",
250
+ size: "small",
251
+ type: "primary",
252
+ loading:
253
+ this
254
+ .loading,
255
+ onClick:
256
+ async () => {
257
+ this.loading = true;
258
+
259
+ await axios.post(
260
+ `api/teams/${$route.params.teamsId}/users`,
261
+ {
262
+ id: model.id,
263
+ },
264
+ );
265
+
266
+ this.emitter.emit(
267
+ "reload-teams",
268
+ );
269
+ },
270
+ },
271
+ "Add",
272
+ );
273
+ },
274
+ }),
275
+ ];
276
+
277
+ const props = {
278
+ model,
279
+ $store,
280
+ $router,
281
+ $route,
282
+ ...rest,
283
+ resource,
284
+ };
285
+
286
+ return h(
287
+ VelTableSorter,
288
+ merge(
289
+ resource.index.structure(
290
+ props,
291
+ ),
292
+ {
293
+ ref: "users",
294
+ },
295
+ ),
296
+ );
297
+ })(),
298
+ ]);
299
+ },
300
+ }),
301
+ };
302
+ },
303
+ ],
304
+ },
305
+ },
306
+ ),
307
+ },
308
+ ];
@@ -26,8 +26,8 @@ export default [
26
26
  {
27
27
  api: {
28
28
  params: {
29
- index: { include: "company" },
30
- show: { include: "company" },
29
+ index: () => ({ include: "company" }),
30
+ show: () => ({ include: "company" }),
31
31
  },
32
32
  },
33
33
  searchable: {
@@ -201,12 +201,14 @@ export default [
201
201
  show: {
202
202
  layout: [
203
203
  ...defaultResource.show.layout,
204
- ({ model }) =>
205
- h(VelFormRole, {
204
+ ({ model }) => ({
205
+ label: "Access control",
206
+ component: h(VelFormRole, {
206
207
  overrides: model.overrides_roles_and_permissions,
207
208
  form: { roles: model.roles.map((d) => d.id) },
208
209
  readonly: true,
209
210
  }),
211
+ }),
210
212
  ],
211
213
  },
212
214
  },
@@ -53,7 +53,7 @@ export default {
53
53
  .get(
54
54
  `${this.resource.api.endpoint(this)}/${this.$route.params[this.resource.id]}`,
55
55
  {
56
- params: this.resource.api.params.show,
56
+ params: this.resource.api.params.show(this),
57
57
  },
58
58
  )
59
59
  .then((res) => {
@@ -14,23 +14,42 @@
14
14
  :title="`${model.name ?? model.id} ${model.last_name ?? ''}`"
15
15
  >
16
16
  <template
17
- v-for="(render, index) in resource.show.actions"
17
+ v-for="(rendered, index) in renderedActions"
18
18
  :key="index"
19
19
  >
20
- <component :is="render(this)" />
20
+ <component :is="rendered" />
21
21
  </template>
22
22
  </VelPageHeader>
23
+
23
24
  <hr class="my-3 hr-muted" />
24
- <template
25
- v-for="(render, index) in resource.show.layout"
26
- :key="index"
27
- >
28
- <component :is="render(this)" />
29
- <hr
30
- v-if="index < resource.show.layout.length - 1"
31
- class="my-3 hr-muted"
32
- />
33
- </template>
25
+
26
+ <el-tabs v-model="active" type="card">
27
+ <template
28
+ v-for="(rendered, index) in renderedLayout"
29
+ :key="index"
30
+ >
31
+ <el-tab-pane :name="index">
32
+ <template #label>
33
+ <span class="align-middle-dive">
34
+ <GIcon
35
+ v-if="rendered.icon"
36
+ class="icon icon--0.5 mr-0.5"
37
+ :name="rendered.icon"
38
+ asis
39
+ embed
40
+ />
41
+ <span>{{
42
+ rendered.label ||
43
+ `Tab ${index + 1}`
44
+ }}</span>
45
+ </span>
46
+ </template>
47
+ <component
48
+ :is="rendered.component || rendered"
49
+ />
50
+ </el-tab-pane>
51
+ </template>
52
+ </el-tabs>
34
53
  </div>
35
54
  </template>
36
55
  <div v-else class="absolute transform-center text-center">
@@ -45,6 +64,7 @@
45
64
  import axios from "axios";
46
65
  import VelSpinner from "../../../components/form/Spinner.vue";
47
66
  import VelButton from "../../../components/basic/Button.vue";
67
+ import { ElTabs, ElTabPane } from "element-plus";
48
68
 
49
69
  export default {
50
70
  components: {
@@ -52,6 +72,8 @@ export default {
52
72
  .default,
53
73
  VelSpinner,
54
74
  VelButton,
75
+ ElTabs,
76
+ ElTabPane,
55
77
  },
56
78
 
57
79
  props: {
@@ -72,6 +94,7 @@ export default {
72
94
  data() {
73
95
  return {
74
96
  model: null,
97
+ active: 0,
75
98
  };
76
99
  },
77
100
 
@@ -80,6 +103,16 @@ export default {
80
103
  deepestRoute() {
81
104
  return this.depth === this.$route.matched.length;
82
105
  },
106
+
107
+ // Compute rendered layout once
108
+ renderedLayout() {
109
+ return this.resource.show.layout.map((render) => render(this));
110
+ },
111
+
112
+ // Compute rendered actions once
113
+ renderedActions() {
114
+ return this.resource.show.actions.map((render) => render(this));
115
+ },
83
116
  },
84
117
 
85
118
  mounted() {
@@ -91,7 +124,7 @@ export default {
91
124
  .get(
92
125
  `${this.resource.api.endpoint(this)}/${this.$route.params[`${this.resource.slug}Id`]}`,
93
126
  {
94
- params: this.resource.api.params.show,
127
+ params: this.resource.api.params.show(this),
95
128
  },
96
129
  )
97
130
  .then((res) => {
@@ -35,8 +35,8 @@ export function meta(name = "default", properties = {}) {
35
35
  api: {
36
36
  endpoint: () => `/api/${properties.slug || kebabCase(name)}`,
37
37
  params: {
38
- index: {},
39
- show: {},
38
+ index: () => ({}),
39
+ show: () => ({}),
40
40
  },
41
41
  },
42
42
  permissions: {
@@ -197,7 +197,7 @@ export function meta(name = "default", properties = {}) {
197
197
  ),
198
198
  api: resource.api.endpoint(props),
199
199
  },
200
- apiParams: resource.api.params.index,
200
+ apiParams: resource.api.params.index(props),
201
201
  idKey: resource.id,
202
202
  "fixed-height": false,
203
203
  displayActions: false,
@@ -324,35 +324,43 @@ export function meta(name = "default", properties = {}) {
324
324
  (props) => {
325
325
  const { resource, model } = props;
326
326
 
327
- return h(
328
- ElDescriptions,
329
- {
330
- border: true,
331
- column: 1,
332
- },
333
- () =>
334
- resource.description.structure.map(
335
- (item, index) =>
336
- h(
337
- ElDescriptionsItem,
338
- {
339
- key: index,
340
- labelWidth: "20%",
341
- },
342
- {
343
- label: () =>
344
- item.label ||
345
- item.key[0].toUpperCase() +
346
- item.key.slice(1),
347
- default: () =>
348
- item.render
349
- ? h(item.render(props))
350
- : model?.[item.key] ||
351
- "",
352
- },
353
- ),
354
- ),
355
- );
327
+ return {
328
+ label: "Details",
329
+ component: h(
330
+ ElDescriptions,
331
+ {
332
+ border: true,
333
+ column: 1,
334
+ },
335
+ () =>
336
+ resource.description.structure.map(
337
+ (item, index) =>
338
+ h(
339
+ ElDescriptionsItem,
340
+ {
341
+ key: index,
342
+ labelWidth: "20%",
343
+ },
344
+ {
345
+ label: () =>
346
+ item.label ||
347
+ item.key[0].toUpperCase() +
348
+ item.key.slice(1),
349
+ default: () =>
350
+ item.render
351
+ ? h(
352
+ item.render(
353
+ props,
354
+ ),
355
+ )
356
+ : model?.[
357
+ item.key
358
+ ] || "",
359
+ },
360
+ ),
361
+ ),
362
+ ),
363
+ };
356
364
  },
357
365
  ],
358
366
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishawack/lab-velocity",
3
- "version": "2.0.0-beta.27",
3
+ "version": "2.0.0-beta.29",
4
4
  "description": "Avalere Health branded style system",
5
5
  "scripts": {
6
6
  "setup": "npm ci || npm i && npm run content",