@hostlink/nuxt-light 1.1.7 → 1.2.1

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/dist/module.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "light",
3
3
  "configKey": "light",
4
- "version": "1.1.7"
4
+ "version": "1.2.1"
5
5
  }
@@ -1,11 +1,12 @@
1
1
  <script setup>
2
- import { useLight } from "#imports";
3
- import { useQuasar } from 'quasar';
2
+ import { useRoute } from 'vue-router';
3
+ import { useLight, q, m } from "#imports";
4
+ import { useQuasar, Loading, Dialog } from 'quasar';
4
5
  import { useI18n } from 'vue-i18n';
5
- import { q, m, f } from '../';
6
6
  import { ref, computed, reactive, provide, watch } from 'vue';
7
7
  import { useRuntimeConfig } from 'nuxt/app';
8
8
 
9
+ Loading.show()
9
10
  //download system value
10
11
  /* import { download } from './../lib/SystemValue'
11
12
  await download();
@@ -20,15 +21,18 @@ const tt = await q({
20
21
  app: ["menus", "viewAsMode", "languages",
21
22
  "copyrightYear",
22
23
  "copyrightName",
24
+ "hasFavorite",
23
25
  { i18nMessages: ["name", "value"] }],
24
- my: ['username', 'first_name', 'last_name', 'roles', "styles", "language", f('granted_storage:granted', { right: "system.storage" }, [])],
26
+ my: ['username', 'first_name', 'last_name', 'roles', "styles", "language", "permissions", { myFavorites: ["label", "path"] }],
25
27
  })
26
28
 
27
29
  let app = tt.app
28
- let my = tt.my
30
+ let my = reactive(tt.my)
29
31
 
30
32
  const light = useLight();
31
33
  light.init(my.styles);
34
+ //set permission
35
+ light.setPermissions(my.permissions);
32
36
 
33
37
  quasar.dark.set(light.isDarkMode());
34
38
 
@@ -36,7 +40,8 @@ const i18n = useI18n();
36
40
  i18n.locale = my.language || 'en';
37
41
 
38
42
  let system = null
39
- if (my.granted_storage) {
43
+
44
+ if (light.isGranted("system.storage")) {
40
45
  system = await q("system", ["diskFreeSpace", "diskUsageSpace", "diskTotalSpace", "diskFreeSpacePercent"]);
41
46
  }
42
47
 
@@ -65,6 +70,7 @@ const toggleRightDrawer = () => {
65
70
  let showViewAs = false;
66
71
  if (my && my.roles.indexOf('Administrators') != -1) {
67
72
  showViewAs = true;
73
+ light.isAdmin = true;
68
74
  }
69
75
 
70
76
  const menuOverlayHeader = ref(false)
@@ -188,19 +194,64 @@ const containerStyle = computed(() => {
188
194
  }
189
195
  })
190
196
 
191
- const fullscreen = ref(document.fullscreenElement)
197
+ const route = useRoute()
198
+
199
+ const reloadMyFavorites = async () => {
200
+ const data = await q({
201
+ my: {
202
+ myFavorites: {
203
+ label: true,
204
+ path: true,
205
+ }
206
+ }
207
+ });
208
+ my.myFavorites = data.my.myFavorites;
209
+ }
210
+
211
+ const onToggleFav = async () => {
192
212
 
193
- const onFullscreen = () => {
194
- if (document.fullscreenElement) {
195
- document.exitFullscreen();
196
- fullscreen.value = false;
197
- } else {
198
- fullscreen.value = true;
213
+ if (isFav.value) {
214
+ await m("removeMyFavorite", {
215
+ path: route.fullPath,
216
+ })
199
217
 
200
- document.documentElement.requestFullscreen();
218
+ //reload my.myFavorites
219
+ reloadMyFavorites();
220
+
221
+ return;
201
222
  }
223
+
224
+ Dialog.create({
225
+ title: 'Add to favorite',
226
+ message: 'Enter favorite label',
227
+ prompt: {
228
+ //get window title, remove - and replace with space
229
+ model: route.name.replace(/-/g, ' '),
230
+ },
231
+ cancel: true,
232
+ persistent: true,
233
+ }).onOk(async (data) => {
234
+ if (data === '') return;
235
+ if (await m("addMyFavorite", {
236
+ path: route.fullPath,
237
+ label: data,
238
+ })) {
239
+ reloadMyFavorites();
240
+
241
+ }
242
+ })
202
243
  }
203
244
 
245
+ const isFav = computed(() => {
246
+ //check my.myFavorites
247
+ let fav = my.myFavorites.find((item) => {
248
+ return item.path == route.fullPath;
249
+ })
250
+ return fav;
251
+ })
252
+
253
+ Loading.hide()
254
+
204
255
  </script>
205
256
 
206
257
  <template>
@@ -218,26 +269,33 @@ const onFullscreen = () => {
218
269
 
219
270
  <q-space />
220
271
 
221
- <q-btn :icon="fullscreen ? 'fullscreen_exit' : 'fullscreen'" round flat dense class="q-mr-sm"
222
- @click="onFullscreen">
272
+ <q-btn :icon="isFav ? 'favorite' : 'sym_o_favorite'" round flat dense class="q-mr-xs" @click="onToggleFav"
273
+ v-if="app.hasFavorite">
274
+ </q-btn>
223
275
 
276
+ <q-btn :icon="$q.fullscreen.isActive ? 'fullscreen_exit' : 'fullscreen'" round flat dense class="q-mr-xs"
277
+ @click="$q.fullscreen.toggle()">
224
278
  </q-btn>
225
279
 
226
- <q-btn v-if="languages.length > 1" rounded flat icon="language" :label="my.language" class="q-mr-sm">
280
+ <q-btn v-if="languages.length > 1" round flat icon="language" class="q-mr-xs">
281
+ <q-tooltip>
282
+ {{ my.language }}
283
+ </q-tooltip>
227
284
  <q-menu>
228
285
  <q-list>
229
- <q-item v-for="language in languages" :key="language.value" v-close-popup clickable
230
- @click="onChangeLocale(language.value)">
286
+ <q-item v-for=" language in languages " :key="language.value" v-close-popup clickable
287
+ @click="onChangeLocale(language.value)" :active="language.value == my.language">
231
288
  <q-item-section>
232
289
  <q-item-label>{{ language.name }}</q-item-label>
233
290
  </q-item-section>
234
291
  </q-item>
235
292
  </q-list>
236
293
  </q-menu>
294
+
295
+
237
296
  </q-btn>
238
297
 
239
- <q-btn icon="sym_o_storage" flat round dense class="q-mr-sm" v-if="my.granted_storage"
240
- :color="storageColor">
298
+ <q-btn icon="sym_o_storage" flat round dense class="q-mr-sm" v-if="system" :color="storageColor">
241
299
  <q-menu>
242
300
  <q-card style="width:250px">
243
301
  <q-card-section>
@@ -315,7 +373,12 @@ const onFullscreen = () => {
315
373
  @mouseover="isMouseOnDrawer = true">
316
374
  <!-- drawer content -->
317
375
  <q-scroll-area class="fit">
318
- <l-menu v-for="menu in menus" :value="menu" :dense="style.dense" />
376
+ <div class="q-mx-sm">
377
+ <l-fav-menu :value="my.myFavorites" :dense="style.dense" v-if="my.myFavorites.length > 0" />
378
+ <l-menu v-for=" menu in menus " :value="menu" :dense="style.dense" />
379
+
380
+ </div>
381
+
319
382
  </q-scroll-area>
320
383
  </q-drawer>
321
384
 
@@ -331,7 +394,7 @@ const onFullscreen = () => {
331
394
  <q-page-container :class="containerClass" :style="containerStyle">
332
395
 
333
396
  <!-- Error message -->
334
- <q-banner dense inline-actions class="bg-grey-4 q-ma-md" v-for="error in errors" rounded>
397
+ <q-banner dense inline-actions class="bg-grey-4 q-ma-md" v-for=" error in errors " rounded>
335
398
  {{ error }}
336
399
  <template v-slot:action>
337
400
  <q-btn flat icon="sym_o_close" round dense @click="light.removeError(error)" />
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { type QBtnProps } from "quasar";
3
- import { ref, computed } from "vue";
4
- import { q, f, useLight } from '#imports';
3
+ import { computed } from "vue";
4
+ import { useLight } from '#imports';
5
5
 
6
6
  export interface LBtnProps extends QBtnProps {
7
7
  permission?: string;
@@ -11,17 +11,6 @@ const props = defineProps<LBtnProps>();
11
11
 
12
12
  const light = useLight();
13
13
 
14
- const granted = ref(false);
15
- if (props.permission) {
16
- const my = await q("my", [f("granted", { right: props.permission }, [])]);
17
-
18
- if (my.granted) {
19
- granted.value = true;
20
- }
21
- } else {
22
- granted.value = true;
23
- }
24
-
25
14
  const attrs = computed(() => {
26
15
  return {
27
16
  ...{
@@ -37,7 +26,7 @@ const attrs = computed(() => {
37
26
 
38
27
  </script>
39
28
  <template>
40
- <q-btn v-bind="attrs" color="primary" v-if="granted">
29
+ <q-btn v-bind="attrs" color="primary" v-if="$light.isGranted(permission)">
41
30
  <slot></slot>
42
31
  </q-btn>
43
32
  </template>
@@ -1,11 +1,15 @@
1
1
  <script setup lang="ts">
2
- import { useLight } from '#imports'
2
+ import { useLight, q, m } from '#imports'
3
3
  import { ref, computed } from 'vue'
4
4
  import { type QCardProps } from 'quasar';
5
5
 
6
6
  export interface LCardProps extends QCardProps {
7
7
  loading?: boolean;
8
8
  title?: string;
9
+ /**
10
+ * Permission to access this card, if not granted, the card will not be shown, if the user is admin, a lock icon will be shown to allow the user to grant the permission
11
+ */
12
+ permission?: string
9
13
  }
10
14
 
11
15
  const light = useLight();
@@ -40,18 +44,98 @@ const fullScreenIcon = computed(() => fullScreen.value ? "sym_o_fullscreen_exit"
40
44
 
41
45
  const showBody = computed(() => !minimize.value || fullScreen.value);
42
46
 
47
+ const showBar = computed(() => {
48
+ if (props.title !== undefined) return true;
49
+ if (showSecurity.value) return true;
50
+ return false;
51
+ });
52
+
53
+
54
+ const showSecurity = computed(() => {
55
+ if (props.permission === undefined) return false;
56
+ if (!light.isAdmin) return false;
57
+ return true;
58
+ });
59
+
60
+
61
+ const roles = ref<{
62
+ name: string;
63
+ granted: boolean;
64
+ }[]>([]);
65
+
66
+ if (props.permission && light.isAdmin) {
67
+ //get role
68
+ const data = await q({
69
+ listRole: {
70
+ name: true
71
+ },
72
+ listPermission: {
73
+ __args: {
74
+ filters: {
75
+ value: props.permission
76
+ }
77
+ }, data: {
78
+ role: true
79
+ }
80
+
81
+ }
82
+ });
83
+
84
+ roles.value = (data.listRole.map((r: any) => {
85
+ return {
86
+ name: r.name,
87
+ granted: data.listPermission.data.find((p: any) => p.role == r.name) != undefined
88
+ }
89
+ }));
90
+ }
91
+
92
+ const onTogglePermission = async (role: any) => {
93
+
94
+ if (role.granted) {
95
+
96
+ //remove permission
97
+ if (await m("removePermission", {
98
+ role: role.name,
99
+ value: props.permission,
100
+ })) {
101
+ role.granted = false;
102
+ }
103
+
104
+ return;
105
+ }
106
+
107
+ if (await m("addPermission", {
108
+ role: role.name,
109
+ value: props.permission,
110
+ })) {
111
+ role.granted = true;
112
+ }
113
+ }
114
+
115
+
116
+
43
117
  </script>
44
118
  <template>
45
- <q-card v-bind="attrs" :class="{ 'fullscreen': fullScreen, 'no-margin': fullScreen }">
46
- <q-bar :class="cl" v-if="title">
47
- <div>{{ $t(title) }}</div>
119
+ <q-card v-bind="attrs" :class="{ 'fullscreen': fullScreen, 'no-margin': fullScreen }"
120
+ v-if="$light.isGranted(permission)" :dark="$q.dark.isActive">
121
+ <q-bar :class="cl" v-if="showBar">
122
+ <div>{{ $t(title ?? "") }}</div>
48
123
  <q-space />
49
- <!-- q-btn-dropdown dense flat icon="sym_o_search" persistent>
50
- <div class="q-ma-md">
51
- <q-input></q-input>
52
- </div>
53
-
54
- </q-btn-dropdown -->
124
+
125
+ <q-btn dense flat icon="sym_o_lock" persistent v-if="showSecurity">
126
+ <q-menu>
127
+ <q-list>
128
+ <q-item clickable v-for="role in roles" @click="onTogglePermission(role)">
129
+ <q-item-section>{{ role.name }}</q-item-section>
130
+ <q-item-section side>
131
+ <q-icon name="sym_o_done" v-if="role.granted" />
132
+ </q-item-section>
133
+ </q-item>
134
+
135
+ </q-list>
136
+ </q-menu>
137
+ </q-btn>
138
+
55
139
  <q-btn dense flat @click="fullScreen = !fullScreen" :icon="fullScreenIcon" />
56
140
  <q-btn dense flat :icon="icon" @click="minimize = !minimize" v-if="!fullScreen" />
57
141
  </q-bar>
@@ -0,0 +1,26 @@
1
+ <script setup>
2
+ const props = defineProps(["value", "dense"])
3
+
4
+ const onClickRemove = () => {
5
+ console.log("onClickRemove")
6
+ }
7
+
8
+ </script>
9
+
10
+ <style scoped>
11
+ .menu-list .q-item{border-radius:12px 12px 12px 12px}.menu-list .q-router-link--exact-active{background:linear-gradient(118deg,var(--q-primary),rgba(115,103,240,.7));color:#fff}
12
+ </style>
13
+ <template>
14
+ <q-expansion-item :label="$t('My Favorite')" :dense="dense" ref="expansion" icon="sym_o_favorite">
15
+ <q-list class="q-pl-md menu-list">
16
+ <q-item v-ripple :to="item.path" v-for="item in value">
17
+ <q-item-section avatar>
18
+ <q-icon name="sym_o_link" />
19
+ </q-item-section>
20
+ <q-item-section>
21
+ <q-item-label v-text="$t(item.label)"></q-item-label>
22
+ </q-item-section>
23
+ </q-item>
24
+ </q-list>
25
+ </q-expansion-item>
26
+ </template>
@@ -194,16 +194,16 @@ onMounted(() => {
194
194
  <q-form ref="form1">
195
195
  <div class="q-gutter-sm">
196
196
  <l-input v-model.trim="data.username" label="Username" :rules="[v => !!v || $t('Username is required')]"
197
- clearable :outlined="false" stackLabel/>
197
+ clearable :outlined="false" stackLabel autocomplete="username" />
198
198
  <l-input v-model="data.password" label="Password" type="password" clearable show-password stackLabel
199
- :rules="[v => !!v || $t('Password is required')]" @keydown.enter.prevent="submit" :outlined="false" />
199
+ :rules="[v => !!v || $t('Password is required')]" @keydown.enter.prevent="submit" :outlined="false" autocomplete="current-password" />
200
200
  <l-input v-if="twoFactorAuthentication" v-model="data.code" label="2FA code" required type="text" clearable>
201
201
  </l-input>
202
202
  </div>
203
203
  </q-form>
204
204
  </q-card-section>
205
205
  <q-card-actions>
206
- <l-btn label="Login" outline rounded color="primary" icon="sym_o_login" @click="submit" />
206
+ <l-btn label="Login" outline rounded color="primary" icon="sym_o_login" @click="submit"/>
207
207
  <l-btn v-if="hasBioLogin" outline rounded color="primary" icon="sym_o_fingerprint" @click="bioLogin" />
208
208
  <l-btn label="Forget password" outline rounded color="primary" icon="sym_o_lock_reset" @click="forgetPassword" />
209
209
  </q-card-actions>
@@ -3,7 +3,6 @@ import { ref } from 'vue'
3
3
  const props = defineProps(["value", "dense"])
4
4
  const menus = ref(null);
5
5
 
6
-
7
6
  const expansion = ref(null);
8
7
 
9
8
  const onShowChild = (menu) => {
@@ -28,7 +27,7 @@ defineExpose({
28
27
  </script>
29
28
 
30
29
  <style scoped>
31
- .menu-list .q-item{border-radius:24px 24px 24px 24px}.menu-list .q-router-link--exact-active{background:linear-gradient(118deg,var(--q-primary),rgba(115,103,240,.7));color:#fff}
30
+ .menu-list .q-item{border-radius:12px 12px 12px 12px}.menu-list .q-router-link--exact-active{background:linear-gradient(118deg,var(--q-primary),rgba(115,103,240,.7));color:#fff}
32
31
  </style>
33
32
  <template>
34
33
  <q-expansion-item v-if="value.children?.length > 0" :label="$t(value.label)" :icon="value.icon" :dense="dense"
@@ -39,7 +38,9 @@ defineExpose({
39
38
  </q-list>
40
39
  </q-expansion-item>
41
40
  <q-list v-else class="menu-list" :dense="dense">
42
- <q-item v-ripple :to="value.to">
41
+ <q-separator v-if="value.type == 'separator'" :spaced="value.spaced" />
42
+ <q-item-label header v-if="value.type=='header'">{{ value.label }}</q-item-label>
43
+ <q-item v-ripple :to="value.to" v-if="!value.type" >
43
44
  <q-item-section avatar>
44
45
  <q-icon :name="value.icon" />
45
46
  </q-item-section>
@@ -2,10 +2,10 @@
2
2
  import { computed } from "vue"
3
3
 
4
4
  export interface LSmallBoxProps {
5
- title: string;
6
- subtitle: string;
7
- icon: string;
8
- color: 'red' | 'pink' | 'purple' | 'deep-purple' | 'indigo' | 'blue' | 'light-blue' | 'cyan' | 'teal' | 'green' | 'light-green' | 'lime' | 'yellow' | 'amber' | 'orange' | 'deep-orange' | 'brown' | 'grey' | 'blue-grey'
5
+ title?: string;
6
+ subtitle?: string;
7
+ icon?: string;
8
+ color?: 'red' | 'pink' | 'purple' | 'deep-purple' | 'indigo' | 'blue' | 'light-blue' | 'cyan' | 'teal' | 'green' | 'light-green' | 'lime' | 'yellow' | 'amber' | 'orange' | 'deep-orange' | 'brown' | 'grey' | 'blue-grey'
9
9
  }
10
10
 
11
11
  const props = withDefaults(defineProps<LSmallBoxProps>(), {
@@ -37,13 +37,15 @@ const localValue = computed({
37
37
  }
38
38
  })
39
39
 
40
- const color = light.getStyle("color");
40
+
41
41
 
42
42
  </script>
43
43
 
44
44
  <template>
45
45
  <l-card>
46
- <q-tabs class="text-grey" :active-color="color" :indicator-color="color" align="justify" v-model="localValue">
46
+
47
+ <q-tabs class="text-grey" :active-color="$light.color" :indicator-color="$light.color" align="justify"
48
+ v-model="localValue">
47
49
  <q-tab v-for="tab in tabContents" :label="$t(tab.label)" :name="tab.name"></q-tab>
48
50
  </q-tabs>
49
51
  <q-tab-panels v-model="localValue">
@@ -0,0 +1,29 @@
1
+ <script setup lang="ts">
2
+ import { model, q } from "#imports";
3
+ const props = defineProps(['id']);
4
+ </script>
5
+ <template>
6
+ <div>
7
+ <l-table row-key="eventlog_id" sort-by="eventlog_id:desc"
8
+ :columns="model('EventLog').columns(['eventlog_id', 'class', 'id', 'action', 'created_time'])" @request="async (req) => {
9
+ const a = {
10
+ listUser: {
11
+ __args: {
12
+ filters: {
13
+ user_id: props.id
14
+ }
15
+ },
16
+ data: {
17
+ __args: {
18
+ limit: 1
19
+ },
20
+ eventLog: req.gql
21
+ }
22
+ }
23
+ }
24
+
25
+ let resp = await q(a);
26
+ req.setData(resp.listUser.data[0].eventLog)
27
+ }" />
28
+ </div>
29
+ </template>
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+
3
+ import { model } from "#imports";
4
+ const props = defineProps(['id']);
5
+
6
+ const obj = await model('User').get({ user_id: props.id }, ["user_id", "username", "first_name", "last_name", "email", "phone", "roles", 'status', 'join_date'])
7
+
8
+ </script>
9
+ <template>
10
+ <l-card class="q-mb-md">
11
+ <q-card-section>
12
+ <l-row>
13
+ <l-col md="5">
14
+ <q-list dense separator>
15
+ <q-item>
16
+ <q-item-section>
17
+ <q-item-label>{{ $t('Username') }}</q-item-label>
18
+ </q-item-section>
19
+ <q-item-section>{{ obj.username }}</q-item-section>
20
+ </q-item>
21
+ <q-item>
22
+ <q-item-section>
23
+ <q-item-label>{{ $t('First name') }}</q-item-label>
24
+ </q-item-section>
25
+ <q-item-section>{{ obj.first_name }}</q-item-section>
26
+ </q-item>
27
+ <q-item>
28
+ <q-item-section>
29
+ <q-item-label>{{ $t('Last name') }}</q-item-label>
30
+ </q-item-section>
31
+ <q-item-section>{{ obj.last_name }}</q-item-section>
32
+ </q-item>
33
+ <q-item>
34
+ <q-item-section>
35
+ <q-item-label>{{ $t('Email') }}</q-item-label>
36
+ </q-item-section>
37
+ <q-item-section>{{ obj.email }}</q-item-section>
38
+ </q-item>
39
+ <q-item>
40
+ <q-item-section>
41
+ <q-item-label>{{ $t('Phone') }}</q-item-label>
42
+ </q-item-section>
43
+ <q-item-section>{{ obj.phone }}</q-item-section>
44
+ </q-item>
45
+
46
+ <q-item>
47
+ <q-item-section>
48
+ <q-item-label>{{ $t('Roles') }}</q-item-label>
49
+ </q-item-section>
50
+ <q-item-section>
51
+ <div class="q-gutter-xs float-left">
52
+ <q-badge v-for="role in obj.roles" :key="role">{{ role }}</q-badge>
53
+ </div>
54
+ </q-item-section>
55
+ </q-item>
56
+
57
+ <q-item>
58
+ <q-item-section>
59
+ <q-item-label>{{ $t('Status') }}</q-item-label>
60
+ </q-item-section>
61
+ <q-item-section>{{ model('User').columns(["status"])[0].format(obj.status) }}</q-item-section>
62
+ </q-item>
63
+
64
+ <q-item>
65
+ <q-item-section>
66
+ <q-item-label>{{ $t('Join date') }}</q-item-label>
67
+ </q-item-section>
68
+ <q-item-section>{{ obj.join_date }}</q-item-section>
69
+ </q-item>
70
+ </q-list>
71
+
72
+
73
+ </l-col>
74
+ </l-row>
75
+
76
+ </q-card-section>
77
+
78
+ </l-card>
79
+ </template>
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ import { model, q } from "#imports";
3
+ const props = defineProps(['id']);
4
+
5
+ const columns = model('UserLog').columns(["userlog_id", "login_dt", "last_access_time", "logout_dt", "result", "user_agent"]);
6
+ </script>
7
+ <template>
8
+ <div>
9
+ <l-table row-key="userlog_id" sort-by="userlog_id:desc" :columns="columns" @request="async (req) => {
10
+ req.loadObjects('UserLog', { filters: { user_id: props.id } })
11
+ }" />
12
+ </div>
13
+ </template>
@@ -14,7 +14,7 @@ const value = computed({
14
14
  set: (val) => props.context.node.input(val)
15
15
  })
16
16
 
17
- console.log(props.context.node.props.parsedRules);
17
+ //console.log(props.context.node.props.parsedRules);
18
18
 
19
19
  //check required in parsedRules
20
20
  let required = false;
@@ -34,7 +34,6 @@ if (required) { //no clearable
34
34
 
35
35
  </script>
36
36
  <template>
37
- {{ required }}
38
37
  <l-select v-model="value" :label="context.label" v-bind="context.attrs" :error="error" :error-message="errorMessage"
39
38
  :clearable="clearable" :required="required">
40
39
  <template v-for="(s, name) in $slots" v-slot:[name]="props" :key="name">
@@ -1,8 +1,43 @@
1
+ declare const app: {
2
+ company: string;
3
+ companyLogo: string;
4
+ color: string;
5
+ theme: string;
6
+ isAdmin: boolean;
7
+ permissions: string[];
8
+ isDarkMode: () => boolean;
9
+ setCompany: (company: string) => void;
10
+ getCompany: () => string;
11
+ setCompanyLogo: (logo: string) => void;
12
+ getCompanyLogo: () => string;
13
+ getVersion: () => any;
14
+ addError: (error: String) => void;
15
+ getErrors: () => String[];
16
+ removeError: (error: String) => void;
17
+ getStyle: (name: String) => any;
18
+ setStyles: (s: Object) => void;
19
+ getStyles: () => {
20
+ [key: string]: any;
21
+ };
22
+ setStyle: (name: String, value: any) => Promise<void>;
23
+ setCurrentRoute: (to: any, from: any) => void;
24
+ getID: () => number | null;
25
+ init: (styles: any) => void;
26
+ isGranted: (right?: string | undefined) => boolean;
27
+ setPermissions: (permissions: string[]) => void;
28
+ };
29
+ declare module 'vue' {
30
+ interface ComponentCustomProperties {
31
+ $light: typeof app;
32
+ }
33
+ }
1
34
  export declare const useLight: () => {
2
35
  company: string;
3
36
  companyLogo: string;
4
37
  color: string;
5
38
  theme: string;
39
+ isAdmin: boolean;
40
+ permissions: string[];
6
41
  isDarkMode: () => boolean;
7
42
  setCompany: (company: string) => void;
8
43
  getCompany: () => string;
@@ -15,12 +50,13 @@ export declare const useLight: () => {
15
50
  getStyle: (name: String) => any;
16
51
  setStyles: (s: Object) => void;
17
52
  getStyles: () => {
18
- theme?: String | undefined;
19
- color?: String | undefined;
53
+ [key: string]: any;
20
54
  };
21
55
  setStyle: (name: String, value: any) => Promise<void>;
22
56
  setCurrentRoute: (to: any, from: any) => void;
23
57
  getID: () => number | null;
24
58
  init: (styles: any) => void;
59
+ isGranted: (right?: string) => boolean;
60
+ setPermissions: (permissions: Array<string>) => void;
25
61
  };
26
62
  export * from "./lib";
@@ -15,6 +15,8 @@ const app = reactive({
15
15
  companyLogo: "",
16
16
  color: "primary",
17
17
  theme: "light",
18
+ isAdmin: false,
19
+ permissions: Array(),
18
20
  isDarkMode: () => {
19
21
  return app.theme == "dark";
20
22
  },
@@ -90,6 +92,18 @@ const app = reactive({
90
92
  watch(() => app.theme, async () => {
91
93
  await app.setStyle("theme", app.theme);
92
94
  });
95
+ },
96
+ isGranted(right) {
97
+ if (right === void 0)
98
+ return true;
99
+ if (app.isAdmin)
100
+ return true;
101
+ if (app.permissions.includes(right))
102
+ return true;
103
+ return false;
104
+ },
105
+ setPermissions(permissions) {
106
+ this.permissions = permissions;
93
107
  }
94
108
  });
95
109
  let currentRoute = null;
@@ -1,12 +1,25 @@
1
1
  <script setup>
2
2
  import { ref, computed, inject } from 'vue';
3
- import { m, q } from '../../../'
3
+ import { m, q } from '#imports'
4
4
  import { useQuasar } from 'quasar';
5
5
 
6
6
  const quasar = useQuasar();
7
- const menus = ref(await q("appMenus", []));
7
+ const appMenus = await q("appMenus", [])
8
+
9
+
10
+ const menus = ref([
11
+ {
12
+ label: "[Root]",
13
+ uuid: "ROOT",
14
+ children: appMenus.map(m => {
15
+ m.parent = 'ROOT'
16
+ return m
17
+ }),
18
+ type: 'root'
19
+ }
20
+ ]);
8
21
 
9
- const selected = ref(null);
22
+ const selected = ref('ROOT');
10
23
  const tree1 = ref(null)
11
24
 
12
25
  const getUUID = () => {
@@ -20,30 +33,9 @@ const getUUID = () => {
20
33
  }
21
34
 
22
35
  const onReload = async () => {
23
- menus.value = await q("appMenus", []);
36
+ menus.value[0].children = await q("appMenus", []);
24
37
  }
25
38
 
26
- const onAdd = () => {
27
-
28
- quasar.dialog({
29
- title: 'Add Menu',
30
- message: 'Enter menu label',
31
- prompt: {
32
- model: '',
33
- },
34
- cancel: true,
35
- persistent: true,
36
- }).onOk((data) => {
37
- if (data === '') return;
38
-
39
- menus.value.push({
40
- label: data,
41
- uuid: getUUID(),
42
- children: [],
43
- });
44
-
45
- });
46
- }
47
39
  const onAddChild = (node) => {
48
40
 
49
41
  quasar.dialog({
@@ -81,9 +73,10 @@ const onRemove = (node) => {
81
73
  }
82
74
  }
83
75
 
84
- const splitterModel = ref(38)
76
+ const splitterModel = ref(20)
85
77
 
86
78
  const selectedNode = computed(() => {
79
+ if (selected.value == 'ROOT') return menus.value[0];
87
80
  return tree1.value.getNodeByKey(selected.value);
88
81
  });
89
82
  const reloadMenu = inject('reloadMenu')
@@ -98,7 +91,7 @@ const onSave = () => {
98
91
  persistent: true,
99
92
  }).onOk(async () => {
100
93
  if (await m("updateAppMenus", {
101
- data: menus.value
94
+ data: menus.value[0].children
102
95
  })) {
103
96
  quasar.notify({
104
97
  message: 'Menu saved',
@@ -140,20 +133,6 @@ const onMoveConfirm = () => {
140
133
  showMove.value = false;
141
134
 
142
135
  }
143
- const onMoveToRoot = () => {
144
- const selectedNode = tree1.value.getNodeByKey(selected.value);
145
-
146
- //clone selected node
147
- const newNode = JSON.parse(JSON.stringify(selectedNode));
148
- newNode.parent = null;
149
- onRemove(selectedNode);
150
-
151
- //add to target
152
-
153
- menus.value.push(newNode);
154
-
155
- showMove.value = false;
156
- }
157
136
 
158
137
  const getParentNode = (node) => {
159
138
  if (!node.parent) return {
@@ -197,6 +176,34 @@ const onAddChildMenu = (node, type) => {
197
176
  children: [],
198
177
  });
199
178
  }
179
+
180
+ const onAddSeparator = (node) => {
181
+ node.children.push({
182
+ label: '[Separator]',
183
+ uuid: getUUID(),
184
+ parent: node.uuid,
185
+ children: [],
186
+ type: 'separator',
187
+ spaced: true,
188
+ });
189
+
190
+
191
+ }
192
+
193
+ const onAddHeader = (node) => {
194
+ node.children.push({
195
+ label: '[Header]',
196
+ uuid: getUUID(),
197
+ parent: node.uuid,
198
+ children: [],
199
+ type: 'header',
200
+ });
201
+ }
202
+
203
+
204
+ const menusOnly = computed(() => {
205
+ return menus.value.filter((item) => !item.type);
206
+ });
200
207
  </script>
201
208
 
202
209
  <template>
@@ -211,79 +218,109 @@ const onAddChildMenu = (node, type) => {
211
218
  <q-card-actions align="right">
212
219
  <q-btn flat label="Cancel" color="primary" v-close-popup />
213
220
  <q-btn flat label="Move" color="primary" @click="onMoveConfirm" />
214
- <q-btn flat label="Move to root" color="primary" @click="onMoveToRoot" />
221
+ <!-- q-btn flat label="Move to root" color="primary" @click="onMoveToRoot" /-->
215
222
  </q-card-actions>
216
223
  </l-card>
217
224
  </q-dialog>
218
225
 
219
226
  <l-card>
227
+ <q-card-actions v-if="selectedNode">
228
+ <l-btn @click="onSave" label="Save" icon="sym_o_save" />
229
+ <l-btn @click="onReload" label="Reload" icon="sym_o_refresh" />
230
+ <template v-if="selectedNode.type == 'root'">
231
+ <l-btn color="primary" @click="onAddChild(selectedNode)" label="Add" icon="sym_o_add" />
232
+ <l-btn color="primary" @click="onAddHeader(selectedNode)" label="Add Header" icon="sym_o_add" />
233
+ <l-btn color="primary" @click="onAddSeparator(selectedNode)" label="Add Separator" icon="sym_o_add" />
234
+ </template>
235
+
236
+ <template v-else>
237
+ <q-btn-dropdown color="primary" label="Add" icon="sym_o_add">
238
+ <q-list>
239
+ <q-item clickable v-close-popup @click="onAddChild(selectedNode)">
240
+ <q-item-section>
241
+ <q-item-label>Child</q-item-label>
242
+ </q-item-section>
243
+ </q-item>
244
+ <q-item clickable v-close-popup @click="onAddHeader(selectedNode)">
245
+ <q-item-section>
246
+ <q-item-label>Header</q-item-label>
247
+ </q-item-section>
248
+ </q-item>
249
+ <q-item clickable v-close-popup @click="onAddSeparator(selectedNode)">
250
+ <q-item-section>
251
+ <q-item-label>Separator</q-item-label>
252
+ </q-item-section>
253
+ </q-item>
254
+
255
+ </q-list>
256
+ </q-btn-dropdown>
257
+
258
+ <l-btn color="primary" @click="onRemove(selectedNode)" label="Remove" icon="sym_o_remove" />
259
+
260
+
261
+ <l-btn color="primary" @click="onMove(selectedNode)" label="Move" icon="sym_o_move" />
262
+
263
+ <l-btn color="primary" @click="onMoveUp(selectedNode)" label="Up" icon="sym_o_arrow_upward" />
264
+
265
+ <l-btn color="primary" @click="onMoveDown(selectedNode)" label="Down" icon="sym_o_arrow_downward" />
266
+
267
+
268
+ <q-btn-dropdown color="primary" label="Add menus" icon="sym_o_add">
269
+ <q-list>
270
+ <q-item clickable v-close-popup @click="onAddChildMenu(selectedNode, 'list')">
271
+ <q-item-section>
272
+ <q-item-label>List</q-item-label>
273
+ </q-item-section>
274
+ </q-item>
275
+
276
+ <q-item clickable v-close-popup @click="onAddChildMenu(selectedNode, 'add')">
277
+ <q-item-section>
278
+ <q-item-label>Add</q-item-label>
279
+ </q-item-section>
280
+ </q-item>
281
+
282
+ </q-list>
283
+ </q-btn-dropdown>
284
+ </template>
285
+ </q-card-actions>
220
286
  <q-splitter v-model="splitterModel" style="height:680px">
221
287
  <template #before>
222
- <q-card-actions>
223
- <l-btn @click="onAdd" label="Add" icon="sym_o_add" />
224
- <l-btn @click="onReload" label="Reload" icon="sym_o_refresh" />
225
-
226
- <l-btn @click="onSave" label="Save" icon="sym_o_save" />
227
- </q-card-actions>
228
288
 
229
289
  <q-tree :nodes="menus" selected-color="primary" default-expand-all v-model:selected="selected"
230
290
  node-key="uuid" ref="tree1" />
231
291
  </template>
232
292
 
233
293
  <template #after v-if="selected">
234
-
235
- <q-card-actions>
236
- <l-btn outline rounded color="primary" @click="onRemove(selectedNode)" label="Remove"
237
- icon="sym_o_remove" />
238
-
239
- <l-btn outline rounded color="primary" @click="onAddChild(selectedNode)" label="Add Child"
240
- icon="sym_o_add" />
241
-
242
- <l-btn outline rounded color="primary" @click="onMove(selectedNode)" label="Move"
243
- icon="sym_o_move" />
244
-
245
- <l-btn outline rounded color="primary" @click="onMoveUp(selectedNode)" label="Up"
246
- icon="sym_o_arrow_upward" />
247
-
248
- <l-btn outline rounded color="primary" @click="onMoveDown(selectedNode)" label="Down"
249
- icon="sym_o_arrow_downward" />
250
-
251
-
252
- <q-btn-dropdown color="primary" label="Add menus" outline rounded icon="sym_o_add">
253
- <q-list>
254
- <q-item clickable v-close-popup @click="onAddChildMenu(selectedNode, 'list')">
255
- <q-item-section>
256
- <q-item-label>List</q-item-label>
257
- </q-item-section>
258
- </q-item>
259
-
260
- <q-item clickable v-close-popup @click="onAddChildMenu(selectedNode, 'add')">
261
- <q-item-section>
262
- <q-item-label>Add</q-item-label>
263
- </q-item-section>
264
- </q-item>
265
-
266
- </q-list>
267
- </q-btn-dropdown>
268
-
269
-
270
- </q-card-actions>
271
-
272
-
273
294
  <q-card-section>
274
- <div class="q-gutter-md">
275
- <l-input label="Label" v-model="selectedNode.label" />
276
- <l-input label="To" v-model="selectedNode.to" />
277
- <l-input label="Icon" v-model="selectedNode.icon" hint="example: sym_o_add" />
278
- <l-input label="Permission" v-model="selectedNode.permission" />
279
- <l-input label="UUID" v-model="selectedNode.uuid" readonly />
280
- </div>
281
-
282
- <div>
283
- <a href="https://fonts.google.com/icons" target="_blank">Material Icons</a>
284
- </div>
285
-
295
+ <template v-if="!selectedNode.type">
296
+ <div class="q-gutter-md">
297
+ <l-input label="Label" v-model="selectedNode.label" />
298
+ <l-input label="To" v-model="selectedNode.to" />
299
+ <l-input label="Icon" v-model="selectedNode.icon" hint="example: sym_o_add" />
300
+ <l-input label="Permission" v-model="selectedNode.permission" />
301
+ <l-input label="UUID" v-model="selectedNode.uuid" readonly />
302
+ </div>
303
+
304
+ <div>
305
+ <a href="https://fonts.google.com/icons" target="_blank">Material Icons</a>
306
+ </div>
307
+
308
+ </template>
309
+
310
+ <template v-if="selectedNode.type == 'separator'">
311
+ <div class="q-gutter-md">
312
+ <q-checkbox label="Spaced" v-model="selectedNode.spaced" />
313
+
314
+ </div>
315
+ </template>
316
+
317
+ <template v-if="selectedNode.type == 'header'">
318
+ <div class="q-gutter-md">
319
+ <l-input label="Label" v-model="selectedNode.label" />
320
+ </div>
321
+ </template>
286
322
  </q-card-section>
323
+
287
324
  </template>
288
325
 
289
326
  </q-splitter>
@@ -1,55 +1,41 @@
1
1
  <script setup>
2
- import { model, getObject, getModelFields, q } from '../../../';
2
+ import { useLight } from "#imports";
3
+ import { useRoute } from "vue-router"
4
+ import { ref } from 'vue';
3
5
 
4
- const obj = await getObject(["user_id", "username", "first_name", "last_name", "email", "phone", "roles", 'status', 'join_date']);
6
+ const route = useRoute();
7
+ const light = useLight();
5
8
 
9
+ const tab = ref('overview');
10
+ const splitter = ref(10);
11
+ const id = route.params.user_id;
6
12
  </script>
7
13
 
8
14
  <template>
9
- <l-page>
15
+ <l-page edit-btn>
10
16
  <template #header>
11
17
  <l-btn to="change-password" icon="sym_o_key" permission="user.changePassword" label="Change password"></l-btn>
12
18
  <l-btn to="update-role" icon="sym_o_people" permission="user.role.add" label="Update role"></l-btn>
13
19
  </template>
14
20
 
15
- <l-card class="q-mb-md">
16
- <l-list v-model="obj" :fields="getModelFields('User', [
17
- 'username',
18
- 'first_name',
19
- 'last_name',
20
- 'email',
21
- 'phone',
22
- 'roles',
23
- 'status',
24
- 'join_date'
25
- ])">
26
- </l-list>
27
- </l-card>
28
-
29
- <l-tabs>
30
- <l-tab label="Event log">
31
- <l-table row-key="eventlog_id" sort-by="eventlog_id:desc"
32
- :columns="model('EventLog').columns(['eventlog_id', 'class', 'id', 'action', 'created_time'])" @request="async (req) => {
33
- const a = {
34
- listUser: {
35
- __args: {
36
- filters: {
37
- user_id: obj.user_id
38
- }
39
- },
40
- data: {
41
- __args: {
42
- limit: 1
43
- },
44
- eventLog: req.gql
45
- }
46
- }
47
- }
48
-
49
- let resp = await q(a);
50
- req.setData(resp.listUser.data[0].eventLog)
51
- }" />
52
- </l-tab>
53
- </l-tabs>
21
+
22
+ <q-card flat bordered>
23
+ <q-tabs v-model="tab" :class="`text-${light.color}`" inline-label align="justify">
24
+ <q-tab name="overview" icon="sym_o_person" label="Overview" />
25
+ <q-tab name="userlog" icon="sym_o_description" label="User log" />
26
+ <q-tab name="eventlog" icon="sym_o_description" label="Event log" />
27
+ </q-tabs>
28
+
29
+ <l-user-overview v-if="tab === 'overview'" :id="id" />
30
+ <l-user-userlog v-if="tab === 'userlog'" :id="id" />
31
+ <l-user-eventlog v-if="tab === 'eventlog'" :id="id" />
32
+
33
+
34
+
35
+ </q-card>
36
+
37
+
38
+
39
+
54
40
  </l-page>
55
41
  </template>
@@ -51,29 +51,52 @@ eventLogCols.forEach(col => {
51
51
  <l-btn icon="sym_o_password" to="setting/password" label="Update password" />
52
52
  <l-btn icon="sym_o_key" to="setting/two-factor-auth" label="Two factor auth" />
53
53
  </template>
54
- <div class="q-gutter-md q-mt-md">
55
- <l-card>
56
- <l-list>
57
- <l-item label="Username">{{ my.username }}</l-item>
58
- <l-item label="First name">{{ my.first_name }}</l-item>
59
- <l-item label="Last name">{{ my.last_name }}</l-item>
60
- <l-item label="Email">{{ my.email }}</l-item>
61
- <l-item label="Phone">{{ my.phone }}</l-item>
62
- <l-item label="Address">{{ my.addr1 }} {{ my.addr2 }} {{ my.addr3 }}</l-item>
63
- <l-item label="Join date">{{ my.join_date }}</l-item>
64
- <l-item label="Roles">{{ my.roles.join(",") }}</l-item>
65
- </l-list>
66
- </l-card>
67
-
68
- <l-card title="User Log">
69
- <l-table :rows="my.userLog.data" :columns="userlogColumns" searchable :rows-per-page-options="[0]">
70
- </l-table>
71
- </l-card>
72
-
73
- <l-card title="Event Log">
74
- <l-table :rows="my.eventLog.data" :columns="eventLogCols" searchable :rows-per-page-options="[0]">
75
- </l-table>
76
- </l-card>
77
- </div>
54
+
55
+ <l-row>
56
+ <l-col md="4">
57
+
58
+ <l-card>
59
+
60
+ <q-card-section class="text-h5 text-center">
61
+ {{ my.first_name }} {{ my.last_name }}
62
+ </q-card-section>
63
+
64
+ <q-card-section class="text-center text-center q-pt-none">
65
+ <q-chip v-for="role in my.roles" :key="role" :label="role" :color="$light.color" text-color="white"
66
+ class="q-ma-xs" />
67
+ </q-card-section>
68
+
69
+
70
+ <q-separator />
71
+ <l-list :bordered="false">
72
+
73
+ <q-item-label header>Details</q-item-label>
74
+ <l-item label="Username">{{ my.username }}</l-item>
75
+ <l-item label="Email">{{ my.email }}</l-item>
76
+ <l-item label="Phone">{{ my.phone }}</l-item>
77
+ <l-item label="Address">{{ my.addr1 }} {{ my.addr2 }} {{ my.addr3 }}</l-item>
78
+ <l-item label="Join date">{{ my.join_date }}</l-item>
79
+ </l-list>
80
+ </l-card>
81
+ </l-col>
82
+
83
+ <l-col md="8">
84
+
85
+ <l-tabs>
86
+ <l-tab label="User Log">
87
+ <l-table :rows="my.userLog.data" :columns="userlogColumns" searchable :rows-per-page-options="[0]">
88
+ </l-table>
89
+ </l-tab>
90
+ <l-tab label="Event Log">
91
+ <l-table :rows="my.eventLog.data" :columns="eventLogCols" searchable :rows-per-page-options="[0]">
92
+ </l-table>
93
+ </l-tab>
94
+
95
+ </l-tabs>
96
+
97
+
98
+ </l-col>
99
+ </l-row>
100
+
78
101
  </l-page>
79
102
  </template>
@@ -1,4 +1,4 @@
1
- import { Quasar, Dialog, Notify, Loading } from "quasar";
1
+ import { Quasar, Dialog, Notify, Loading, AppFullscreen } from "quasar";
2
2
  import { createI18n } from "vue-i18n";
3
3
  import { defineNuxtPlugin } from "#app";
4
4
  import { useRouter } from "vue-router";
@@ -24,6 +24,7 @@ export default defineNuxtPlugin((nuxtApp) => {
24
24
  defineModel("SystemValue", TypeSystemValue);
25
25
  defineModel("MailLog", TypeMailLog);
26
26
  defineModel("EventLog", TypeEventLog);
27
+ nuxtApp.vueApp.config.globalProperties.$light = useLight();
27
28
  nuxtApp.vueApp.config.errorHandler = (error) => {
28
29
  console.log(error);
29
30
  const light = useLight();
@@ -58,7 +59,8 @@ export default defineNuxtPlugin((nuxtApp) => {
58
59
  plugins: {
59
60
  Dialog,
60
61
  Notify,
61
- Loading
62
+ Loading,
63
+ AppFullscreen
62
64
  }
63
65
  });
64
66
  let locale = localStorage.getItem("locale") || "en";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostlink/nuxt-light",
3
- "version": "1.1.7",
3
+ "version": "1.2.1",
4
4
  "description": "HostLink Nuxt Light Framework",
5
5
  "repository": "@hostlink/nuxt-light",
6
6
  "license": "MIT",