@fishawack/lab-velocity 2.0.0-beta.26 → 2.0.0-beta.28

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.
@@ -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
  }
@@ -75,6 +75,21 @@
75
75
  </VelButton>
76
76
  </div>
77
77
 
78
+ <VelBasic
79
+ v-model="form.seats"
80
+ name="seats"
81
+ :error="form.errors"
82
+ type="number"
83
+ placeholder="Company seats"
84
+ label="Company seats"
85
+ min="0"
86
+ class="mb-0 mt-2"
87
+ >
88
+ <template #label>
89
+ Company seats <sup class="color-status-red-100">*</sup>
90
+ </template>
91
+ </VelBasic>
92
+
78
93
  <hr class="my-5 hr-muted" />
79
94
 
80
95
  <template v-if="$store.getters.can('edit roles')">
@@ -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: {
@@ -33,6 +34,7 @@ export default [
33
34
  name: model?.name || null,
34
35
  primary_contact: model?.primary_contact?.id || null,
35
36
  domains: model?.domains || [],
37
+ seats: model?.seats != null ? model.seats : null,
36
38
  roles: model?.roles.map((val) => val.id) || [],
37
39
  sso_client_id: model?.sso_client_id || undefined,
38
40
  sso_tenant: model?.sso_tenant || undefined,
@@ -95,9 +97,13 @@ export default [
95
97
  h("span", !!model.primary_contact_contacted),
96
98
  },
97
99
  {
98
- label: "Total users",
100
+ label: "Total Users",
99
101
  key: "user_count",
100
102
  },
103
+ {
104
+ label: "Available Seats",
105
+ key: "seats",
106
+ },
101
107
  ],
102
108
  },
103
109
  index: {
@@ -146,53 +152,106 @@ export default [
146
152
  ],
147
153
  layout: [
148
154
  ...defaultResource.show.layout,
149
- ({ model }) =>
150
- h(VelFormRole, {
155
+ ({ model }) => ({
156
+ label: "Access control",
157
+ component: h(VelFormRole, {
151
158
  overrides: model.overrides_roles_and_permissions,
152
159
  form: { roles: model.roles.map((d) => d.id) },
153
160
  readonly: true,
154
161
  }),
155
- ({ 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 }) => {
156
208
  const resource = meta(...userResource);
157
209
 
158
- 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
+ });
159
214
 
160
215
  const props = {
161
216
  model,
162
217
  $store,
163
218
  $router,
219
+ $route,
164
220
  ...rest,
165
221
  resource,
166
222
  };
167
223
 
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
- });
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
+ },
181
240
  },
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
- ),
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)),
193
253
  ]),
194
- h(VelTableSorter, resource.index.structure(props)),
195
- ]);
254
+ };
196
255
  },
197
256
  ],
198
257
  },
@@ -0,0 +1,258 @@
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 VelTableSorter from "../../../../components/layout/TableSorter.vue";
11
+ import VelButton from "../../../../components/basic/Button.vue";
12
+ import VelCheckbox from "../../../../components/form/Checkbox.vue";
13
+
14
+ export default [
15
+ "teams",
16
+ {
17
+ icon: `icon-account-circle`,
18
+ api: {
19
+ params: {
20
+ index: ({ $route }) => ({
21
+ include: "company",
22
+ "filter[company_id]": $route.params.companiesId,
23
+ }),
24
+ show: () => ({
25
+ include: "company",
26
+ }),
27
+ },
28
+ },
29
+ permissions: {
30
+ create: ({ $store }) => $store.getters.can("write teams"),
31
+ edit: ({ $store }) => $store.getters.can("write teams"),
32
+ delete: ({ $store }) => $store.getters.can("delete teams"),
33
+ },
34
+ ...merge(
35
+ columns([
36
+ {
37
+ key: "name",
38
+ sortable: true,
39
+ },
40
+ {
41
+ key: "description",
42
+ },
43
+ {
44
+ key: "company_id",
45
+ label: "Company",
46
+ class: "hidden",
47
+ render: {
48
+ read: ({ model }) => h("span", model.company.name),
49
+ },
50
+ initial: ({ $route, model }) =>
51
+ model?.company_id || $route.params.companiesId || null,
52
+ },
53
+ ]),
54
+ {
55
+ show: {
56
+ layout: [
57
+ ...defaultResource.show.layout,
58
+ (props) => {
59
+ const { model, $store, $router, $route, ...rest } =
60
+ props;
61
+
62
+ return {
63
+ label: "Members",
64
+ component: h({
65
+ data: () => ({
66
+ scoped: true,
67
+ }),
68
+ mounted() {
69
+ this.emitter.on("reload-teams", () => {
70
+ this.reload();
71
+ });
72
+ },
73
+ beforeUnmount() {
74
+ this.emitter.off("reload-teams");
75
+ },
76
+ methods: {
77
+ reload: throttle(function () {
78
+ this.$refs.members.reload();
79
+ this.$refs.users.reload();
80
+ }, 1000),
81
+ },
82
+ render() {
83
+ return h("div", [
84
+ h("h3", "Members"),
85
+ (() => {
86
+ const resource = meta(
87
+ ...userResource,
88
+ );
89
+
90
+ resource.api.params.index = ({
91
+ $route,
92
+ }) => ({
93
+ include: "company",
94
+ "filter[teams.id]":
95
+ $route.params.teamsId,
96
+ });
97
+
98
+ resource.table.actions = [
99
+ ({ model }) =>
100
+ h({
101
+ data: () => ({
102
+ loading: false,
103
+ }),
104
+ render() {
105
+ return h(
106
+ VelButton,
107
+ {
108
+ tag: "a",
109
+ size: "small",
110
+ type: "warning",
111
+ loading:
112
+ this
113
+ .loading,
114
+ onClick:
115
+ async () => {
116
+ this.loading = true;
117
+
118
+ await axios.delete(
119
+ `api/teams/${$route.params.teamsId}/users/${model.id}`,
120
+ );
121
+
122
+ this.emitter.emit(
123
+ "reload-teams",
124
+ );
125
+
126
+ this.loading = false;
127
+ },
128
+ },
129
+ "Remove",
130
+ );
131
+ },
132
+ }),
133
+ ];
134
+
135
+ const props = {
136
+ model,
137
+ $store,
138
+ $router,
139
+ $route,
140
+ ...rest,
141
+ resource,
142
+ };
143
+
144
+ return h(
145
+ VelTableSorter,
146
+ merge(
147
+ resource.index.structure(
148
+ props,
149
+ ),
150
+ {
151
+ ref: "members",
152
+ },
153
+ ),
154
+ );
155
+ })(),
156
+ h("h3", "Users"),
157
+ h(VelCheckbox, {
158
+ label: "Only show users from this company",
159
+ class: "mt-2",
160
+ modelValue: this.scoped,
161
+ "onUpdate:modelValue": (
162
+ value,
163
+ ) => {
164
+ this.scoped = value;
165
+ this.$nextTick(() => {
166
+ this.$refs.users.reload();
167
+ });
168
+ },
169
+ }),
170
+ (() => {
171
+ const resource = meta(
172
+ ...userResource,
173
+ );
174
+
175
+ resource.api.params.index = ({
176
+ $route,
177
+ }) => ({
178
+ include: "company",
179
+ "filter[company_id]": this
180
+ .scoped
181
+ ? $route.params
182
+ .companiesId
183
+ : null,
184
+ "filter[teams.id]": `!${$route.params.teamsId}`,
185
+ });
186
+
187
+ resource.table.actions = [
188
+ ({ model }) =>
189
+ h({
190
+ data: () => ({
191
+ loading: false,
192
+ }),
193
+ render() {
194
+ return h(
195
+ VelButton,
196
+ {
197
+ tag: "a",
198
+ size: "small",
199
+ type: "primary",
200
+ loading:
201
+ this
202
+ .loading,
203
+ onClick:
204
+ async () => {
205
+ this.loading = true;
206
+
207
+ await axios.post(
208
+ `api/teams/${$route.params.teamsId}/users`,
209
+ {
210
+ id: model.id,
211
+ },
212
+ );
213
+
214
+ this.emitter.emit(
215
+ "reload-teams",
216
+ );
217
+
218
+ this.loading = false;
219
+ },
220
+ },
221
+ "Add",
222
+ );
223
+ },
224
+ }),
225
+ ];
226
+
227
+ const props = {
228
+ model,
229
+ $store,
230
+ $router,
231
+ $route,
232
+ ...rest,
233
+ resource,
234
+ };
235
+
236
+ return h(
237
+ VelTableSorter,
238
+ merge(
239
+ resource.index.structure(
240
+ props,
241
+ ),
242
+ {
243
+ ref: "users",
244
+ },
245
+ ),
246
+ );
247
+ })(),
248
+ ]);
249
+ },
250
+ }),
251
+ };
252
+ },
253
+ ],
254
+ },
255
+ },
256
+ ),
257
+ },
258
+ ];
@@ -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.26",
3
+ "version": "2.0.0-beta.28",
4
4
  "description": "Avalere Health branded style system",
5
5
  "scripts": {
6
6
  "setup": "npm ci || npm i && npm run content",