@hostlink/nuxt-light 1.68.0 → 1.70.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.
Files changed (28) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +3 -1
  3. package/dist/runtime/components/L/App.d.vue.ts +24 -1
  4. package/dist/runtime/components/L/App.vue +16 -1
  5. package/dist/runtime/components/L/App.vue.d.ts +24 -1
  6. package/dist/runtime/components/L/AppMain.d.vue.ts +33 -7
  7. package/dist/runtime/components/L/AppMain.vue +55 -1
  8. package/dist/runtime/components/L/AppMain.vue.d.ts +33 -7
  9. package/dist/runtime/components/L/DialogInsertEditImage.d.vue.ts +20 -0
  10. package/dist/runtime/components/L/DialogInsertEditImage.vue +133 -0
  11. package/dist/runtime/components/L/DialogInsertEditImage.vue.d.ts +20 -0
  12. package/dist/runtime/components/L/DialogInsertEditLink.d.vue.ts +20 -0
  13. package/dist/runtime/components/L/DialogInsertEditLink.vue +64 -0
  14. package/dist/runtime/components/L/DialogInsertEditLink.vue.d.ts +20 -0
  15. package/dist/runtime/components/L/DialogSelectFileFromFileManager.d.vue.ts +9 -0
  16. package/dist/runtime/components/L/DialogSelectFileFromFileManager.vue +25 -0
  17. package/dist/runtime/components/L/DialogSelectFileFromFileManager.vue.d.ts +9 -0
  18. package/dist/runtime/components/L/Editor.vue +237 -15
  19. package/dist/runtime/components/L/FileManager.vue +1 -1
  20. package/dist/runtime/components/L/System/Setting/mail.vue +15 -8
  21. package/dist/runtime/components/L/System/Setting/modules.d.vue.ts +1 -0
  22. package/dist/runtime/components/L/System/Setting/modules.vue +6 -2
  23. package/dist/runtime/components/L/System/Setting/modules.vue.d.ts +1 -0
  24. package/dist/runtime/pages/System/view_as.vue +1 -1
  25. package/dist/runtime/pages/page_not_found.d.vue.ts +3 -0
  26. package/dist/runtime/pages/page_not_found.vue +26 -0
  27. package/dist/runtime/pages/page_not_found.vue.d.ts +3 -0
  28. package/package.json +3 -2
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "light",
3
3
  "configKey": "light",
4
- "version": "1.68.0",
4
+ "version": "1.70.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -236,7 +236,9 @@ export default defineNuxtPlugin(() => {})`;
236
236
  import { query as q } from '@hostlink/light'
237
237
  export default defineNuxtPlugin(() => {
238
238
  addRouteMiddleware("auth", async (to) => {
239
- if (to.path === "/") return;
239
+ const normalize = (p) => (p || "").replace(/[/]+$/, "");
240
+ const publicPaths = ["/", "/page_not_found", "/login"].map(normalize);
241
+ if (publicPaths.includes(normalize(to.path))) return;
240
242
  const { my } = await q({
241
243
  my: { allowedPath: { __args: { path: to.path } } }
242
244
  });
@@ -4,8 +4,31 @@ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
4
4
  type __VLS_WithSlots<T, S> = T & (new () => {
5
5
  $slots: S;
6
6
  });
7
- declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
7
+ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
8
+ userMenuItems: {
9
+ type: ArrayConstructor;
10
+ default: () => never[];
11
+ };
12
+ headerButtons: {
13
+ type: ArrayConstructor;
14
+ default: () => never[];
15
+ };
16
+ }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
17
+ userMenuItems: {
18
+ type: ArrayConstructor;
19
+ default: () => never[];
20
+ };
21
+ headerButtons: {
22
+ type: ArrayConstructor;
23
+ default: () => never[];
24
+ };
25
+ }>> & Readonly<{}>, {
26
+ userMenuItems: unknown[];
27
+ headerButtons: unknown[];
28
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
8
29
  type __VLS_Slots = {
30
+ [x: `header-button-menu-${any}`]: ((props: {}) => any) | undefined;
31
+ } & {
9
32
  header?: ((props: {}) => any) | undefined;
10
33
  } & {
11
34
  'user-menu'?: ((props: {}) => any) | undefined;
@@ -4,6 +4,16 @@ import { useLight, watch } from "#imports";
4
4
  import { useQuasar } from "quasar";
5
5
  import { q } from "#imports";
6
6
  import { useRoute } from "vue-router";
7
+ defineProps({
8
+ userMenuItems: {
9
+ type: Array,
10
+ default: () => []
11
+ },
12
+ headerButtons: {
13
+ type: Array,
14
+ default: () => []
15
+ }
16
+ });
7
17
  const route = useRoute();
8
18
  const light = useLight();
9
19
  const quasar = useQuasar();
@@ -83,7 +93,8 @@ if (app.value.facebookAppId) {
83
93
  </q-page-container>
84
94
  </q-layout>
85
95
 
86
- <l-app-main v-else @logout="app.logged = false" v-bind="app">
96
+ <l-app-main v-else @logout="app.logged = false" v-bind="app" :user-menu-items="userMenuItems"
97
+ :header-buttons="headerButtons">
87
98
  <template #header>
88
99
  <slot name="header"></slot>
89
100
  </template>
@@ -92,6 +103,10 @@ if (app.value.facebookAppId) {
92
103
  <slot name="user-menu"></slot>
93
104
  </template>
94
105
 
106
+ <template v-for="button in headerButtons" :key="button.name" #[`header-button-menu-${button.name}`]>
107
+ <slot :name="`header-button-menu-${button.name}`"></slot>
108
+ </template>
109
+
95
110
  <template #page-top>
96
111
  <slot name="page-top"></slot>
97
112
  </template>
@@ -4,8 +4,31 @@ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
4
4
  type __VLS_WithSlots<T, S> = T & (new () => {
5
5
  $slots: S;
6
6
  });
7
- declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
7
+ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
8
+ userMenuItems: {
9
+ type: ArrayConstructor;
10
+ default: () => never[];
11
+ };
12
+ headerButtons: {
13
+ type: ArrayConstructor;
14
+ default: () => never[];
15
+ };
16
+ }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
17
+ userMenuItems: {
18
+ type: ArrayConstructor;
19
+ default: () => never[];
20
+ };
21
+ headerButtons: {
22
+ type: ArrayConstructor;
23
+ default: () => never[];
24
+ };
25
+ }>> & Readonly<{}>, {
26
+ userMenuItems: unknown[];
27
+ headerButtons: unknown[];
28
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
8
29
  type __VLS_Slots = {
30
+ [x: `header-button-menu-${any}`]: ((props: {}) => any) | undefined;
31
+ } & {
9
32
  header?: ((props: {}) => any) | undefined;
10
33
  } & {
11
34
  'user-menu'?: ((props: {}) => any) | undefined;
@@ -1,18 +1,44 @@
1
- declare var __VLS_38: {}, __VLS_245: {}, __VLS_356: {}, __VLS_363: {};
1
+ interface UserMenuItem {
2
+ label: string;
3
+ icon?: string;
4
+ to?: string;
5
+ click?: () => void;
6
+ visible?: boolean;
7
+ }
8
+ interface HeaderButton {
9
+ name: string;
10
+ icon: string;
11
+ tooltip?: string;
12
+ to?: string;
13
+ click?: () => void;
14
+ menu?: boolean;
15
+ visible?: boolean;
16
+ desktop?: boolean;
17
+ mobile?: boolean;
18
+ }
19
+ type __VLS_Props = {
20
+ userMenuItems?: UserMenuItem[];
21
+ headerButtons?: HeaderButton[];
22
+ };
23
+ declare var __VLS_38: {}, __VLS_61: `header-button-menu-${string}`, __VLS_62: {}, __VLS_132: `header-button-menu-${string}`, __VLS_133: {}, __VLS_340: {}, __VLS_482: {}, __VLS_489: {};
2
24
  type __VLS_Slots = {} & {
25
+ [K in NonNullable<typeof __VLS_61>]?: (props: typeof __VLS_62) => any;
26
+ } & {
27
+ [K in NonNullable<typeof __VLS_132>]?: (props: typeof __VLS_133) => any;
28
+ } & {
3
29
  header?: (props: typeof __VLS_38) => any;
4
30
  } & {
5
- 'user-menu'?: (props: typeof __VLS_245) => any;
31
+ 'user-menu'?: (props: typeof __VLS_340) => any;
6
32
  } & {
7
- 'page-top'?: (props: typeof __VLS_356) => any;
33
+ 'page-top'?: (props: typeof __VLS_482) => any;
8
34
  } & {
9
- 'page-bottom'?: (props: typeof __VLS_363) => any;
35
+ 'page-bottom'?: (props: typeof __VLS_489) => any;
10
36
  };
11
- declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
37
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
38
  logout: (...args: any[]) => void;
13
- }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
39
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
14
40
  onLogout?: ((...args: any[]) => any) | undefined;
15
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
41
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
42
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
17
43
  declare const _default: typeof __VLS_export;
18
44
  export default _default;
@@ -7,6 +7,10 @@ import { ref, computed, reactive, provide, watch, toRaw, onMounted } from "vue";
7
7
  import { useRuntimeConfig } from "nuxt/app";
8
8
  import { logout } from "@hostlink/light";
9
9
  const emits = defineEmits(["logout"]);
10
+ defineProps({
11
+ userMenuItems: { type: Array, required: false },
12
+ headerButtons: { type: Array, required: false }
13
+ });
10
14
  const $q = useQuasar();
11
15
  $q.loading.show();
12
16
  const config = useRuntimeConfig();
@@ -28,6 +32,7 @@ const tt = await q({
28
32
  copyrightName: true,
29
33
  copyrightYear: true,
30
34
  hasFavorite: true,
35
+ notificationBellEnabled: true,
31
36
  i18nMessages: {
32
37
  name: true,
33
38
  value: true
@@ -254,6 +259,45 @@ const onLogout = async () => {
254
259
 
255
260
  <slot name="header"></slot>
256
261
 
262
+ <!-- Header buttons (desktop) -->
263
+ <template v-for="button in headerButtons || []" :key="button.name">
264
+ <q-btn v-if="button.visible !== false && button.desktop !== false" round flat dense
265
+ :icon="button.icon" class="q-mr-sm gt-xs" @click="button.click"
266
+ :to="button.to ? button.to : void 0">
267
+ <q-tooltip v-if="button.tooltip">{{ $t(button.tooltip) }}</q-tooltip>
268
+ <q-menu v-if="button.menu">
269
+ <slot :name="`header-button-menu-${button.name}`"></slot>
270
+ </q-menu>
271
+ </q-btn>
272
+ </template>
273
+
274
+ <!-- Header buttons (mobile hamburger) -->
275
+ <q-btn round flat dense icon="menu" class="q-mr-sm lt-sm" v-if="headerButtons?.length">
276
+ <q-tooltip>More</q-tooltip>
277
+ <q-menu>
278
+ <q-list>
279
+ <template v-for="button in headerButtons || []" :key="button.name">
280
+ <q-item v-if="button.visible !== false && button.mobile !== false"
281
+ :clickable="!button.menu && (!!button.to || !!button.click)" @click="button.click"
282
+ :to="button.to ? button.to : void 0">
283
+ <q-item-section avatar>
284
+ <q-icon :name="button.icon" />
285
+ </q-item-section>
286
+ <q-item-section>{{ button.tooltip ? $t(button.tooltip) : button.name
287
+ }}</q-item-section>
288
+ <q-item-section side v-if="button.menu">
289
+ <q-btn flat dense icon="arrow_right">
290
+ <q-menu anchor="top end" self="top start">
291
+ <slot :name="`header-button-menu-${button.name}`"></slot>
292
+ </q-menu>
293
+ </q-btn>
294
+ </q-item-section>
295
+ </q-item>
296
+ </template>
297
+ </q-list>
298
+ </q-menu>
299
+ </q-btn>
300
+
257
301
  <q-btn :icon="isFav ? 'favorite' : 'sym_o_favorite'" round flat dense class="q-mr-xs"
258
302
  @click="onToggleFav" v-if="app.hasFavorite">
259
303
  </q-btn>
@@ -288,7 +332,7 @@ const onLogout = async () => {
288
332
  </q-menu>
289
333
  </q-btn>
290
334
 
291
- <l-notification-bell class="q-mr-xs" />
335
+ <l-notification-bell class="q-mr-xs" v-if="app.notificationBellEnabled !== false" />
292
336
 
293
337
  <div class="q-mx-sm" v-if="$q.screen.gt.xs">
294
338
  <div class="text-bold text-right">
@@ -336,6 +380,16 @@ const onLogout = async () => {
336
380
 
337
381
  <slot name="user-menu"></slot>
338
382
 
383
+ <q-item v-for="item in userMenuItems || []" :key="item.label" v-close-popup :to="item.to"
384
+ clickable @click="item.click" v-show="item.visible !== false">
385
+ <q-item-section avatar v-if="item.icon">
386
+ <q-icon :name="item.icon" />
387
+ </q-item-section>
388
+ <q-item-section>
389
+ <q-item-label>{{ $t(item.label) }}</q-item-label>
390
+ </q-item-section>
391
+ </q-item>
392
+
339
393
  <q-separator />
340
394
 
341
395
  <q-item @click="onLogout" clickable>
@@ -1,18 +1,44 @@
1
- declare var __VLS_38: {}, __VLS_245: {}, __VLS_356: {}, __VLS_363: {};
1
+ interface UserMenuItem {
2
+ label: string;
3
+ icon?: string;
4
+ to?: string;
5
+ click?: () => void;
6
+ visible?: boolean;
7
+ }
8
+ interface HeaderButton {
9
+ name: string;
10
+ icon: string;
11
+ tooltip?: string;
12
+ to?: string;
13
+ click?: () => void;
14
+ menu?: boolean;
15
+ visible?: boolean;
16
+ desktop?: boolean;
17
+ mobile?: boolean;
18
+ }
19
+ type __VLS_Props = {
20
+ userMenuItems?: UserMenuItem[];
21
+ headerButtons?: HeaderButton[];
22
+ };
23
+ declare var __VLS_38: {}, __VLS_61: `header-button-menu-${string}`, __VLS_62: {}, __VLS_132: `header-button-menu-${string}`, __VLS_133: {}, __VLS_340: {}, __VLS_482: {}, __VLS_489: {};
2
24
  type __VLS_Slots = {} & {
25
+ [K in NonNullable<typeof __VLS_61>]?: (props: typeof __VLS_62) => any;
26
+ } & {
27
+ [K in NonNullable<typeof __VLS_132>]?: (props: typeof __VLS_133) => any;
28
+ } & {
3
29
  header?: (props: typeof __VLS_38) => any;
4
30
  } & {
5
- 'user-menu'?: (props: typeof __VLS_245) => any;
31
+ 'user-menu'?: (props: typeof __VLS_340) => any;
6
32
  } & {
7
- 'page-top'?: (props: typeof __VLS_356) => any;
33
+ 'page-top'?: (props: typeof __VLS_482) => any;
8
34
  } & {
9
- 'page-bottom'?: (props: typeof __VLS_363) => any;
35
+ 'page-bottom'?: (props: typeof __VLS_489) => any;
10
36
  };
11
- declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
37
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
38
  logout: (...args: any[]) => void;
13
- }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
39
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
14
40
  onLogout?: ((...args: any[]) => any) | undefined;
15
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
41
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
42
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
17
43
  declare const _default: typeof __VLS_export;
18
44
  export default _default;
@@ -0,0 +1,20 @@
1
+ interface DialogInsertEditImageProps {
2
+ initialSource?: string;
3
+ initialAlt?: string;
4
+ initialWidth?: number | null;
5
+ initialHeight?: number | null;
6
+ }
7
+ declare const __VLS_export: import("vue").DefineComponent<DialogInsertEditImageProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ hide: (...args: any[]) => void;
9
+ ok: (...args: any[]) => void;
10
+ }, string, import("vue").PublicProps, Readonly<DialogInsertEditImageProps> & Readonly<{
11
+ onHide?: ((...args: any[]) => any) | undefined;
12
+ onOk?: ((...args: any[]) => any) | undefined;
13
+ }>, {
14
+ initialSource: string;
15
+ initialAlt: string;
16
+ initialWidth: number | null;
17
+ initialHeight: number | null;
18
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -0,0 +1,133 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ import { useDialogPluginComponent } from "quasar";
4
+ const props = defineProps({
5
+ initialSource: { type: String, required: false, default: "" },
6
+ initialAlt: { type: String, required: false, default: "" },
7
+ initialWidth: { type: [Number, null], required: false, default: null },
8
+ initialHeight: { type: [Number, null], required: false, default: null }
9
+ });
10
+ defineEmits([
11
+ ...useDialogPluginComponent.emits
12
+ ]);
13
+ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
14
+ const source = ref(props.initialSource);
15
+ const alt = ref(props.initialAlt);
16
+ const width = ref(props.initialWidth);
17
+ const height = ref(props.initialHeight);
18
+ const lockAspectRatio = ref(true);
19
+ const aspectRatio = ref(null);
20
+ const canSubmit = computed(() => source.value.trim().length > 0);
21
+ const toPositiveNumber = (value) => {
22
+ const n = Number(value);
23
+ return Number.isFinite(n) && n > 0 ? n : null;
24
+ };
25
+ const updateImageMeta = (src) => {
26
+ return new Promise((resolve) => {
27
+ if (!src) {
28
+ aspectRatio.value = null;
29
+ resolve();
30
+ return;
31
+ }
32
+ const img = new Image();
33
+ img.onload = () => {
34
+ if (img.naturalWidth > 0 && img.naturalHeight > 0) {
35
+ aspectRatio.value = img.naturalWidth / img.naturalHeight;
36
+ if (!width.value) width.value = img.naturalWidth;
37
+ if (!height.value) height.value = img.naturalHeight;
38
+ }
39
+ resolve();
40
+ };
41
+ img.onerror = () => resolve();
42
+ img.src = src;
43
+ });
44
+ };
45
+ watch(() => source.value, (value) => {
46
+ updateImageMeta(value.trim());
47
+ }, { immediate: true });
48
+ const onWidthInput = (val) => {
49
+ const newWidth = toPositiveNumber(val);
50
+ width.value = newWidth;
51
+ if (lockAspectRatio.value && aspectRatio.value && newWidth) {
52
+ height.value = Math.round(newWidth / aspectRatio.value);
53
+ }
54
+ };
55
+ const onHeightInput = (val) => {
56
+ const newHeight = toPositiveNumber(val);
57
+ height.value = newHeight;
58
+ if (lockAspectRatio.value && aspectRatio.value && newHeight) {
59
+ width.value = Math.round(newHeight * aspectRatio.value);
60
+ }
61
+ };
62
+ const toggleLock = () => {
63
+ lockAspectRatio.value = !lockAspectRatio.value;
64
+ if (lockAspectRatio.value && aspectRatio.value && width.value) {
65
+ height.value = Math.round(width.value / aspectRatio.value);
66
+ }
67
+ };
68
+ const onConfirm = () => {
69
+ if (!canSubmit.value) return;
70
+ onDialogOK({
71
+ source: source.value.trim(),
72
+ alt: alt.value.trim(),
73
+ width: width.value,
74
+ height: height.value
75
+ });
76
+ };
77
+ </script>
78
+
79
+ <template>
80
+ <q-dialog ref="dialogRef" @hide="onDialogHide">
81
+ <q-card style="min-width: 560px; max-width: 90vw;">
82
+ <q-card-section class="text-h6">
83
+ Insert/Edit Image
84
+ </q-card-section>
85
+
86
+ <q-card-section class="q-gutter-md">
87
+ <q-input v-model="source" label="Source" outlined dense />
88
+ <q-input v-model="alt" label="Alternative Description" outlined dense />
89
+
90
+ <div class="row items-center q-col-gutter-sm">
91
+ <div class="col">
92
+ <q-input
93
+ :model-value="width"
94
+ @update:model-value="onWidthInput"
95
+ type="number"
96
+ min="1"
97
+ label="Width"
98
+ outlined
99
+ dense
100
+ />
101
+ </div>
102
+ <div class="col-auto">
103
+ <q-btn
104
+ flat
105
+ round
106
+ :icon="lockAspectRatio ? 'sym_o_lock' : 'sym_o_lock_open'"
107
+ :color="lockAspectRatio ? 'primary' : 'grey'"
108
+ @click="toggleLock"
109
+ >
110
+ <q-tooltip>Lock Aspect Ratio</q-tooltip>
111
+ </q-btn>
112
+ </div>
113
+ <div class="col">
114
+ <q-input
115
+ :model-value="height"
116
+ @update:model-value="onHeightInput"
117
+ type="number"
118
+ min="1"
119
+ label="Height"
120
+ outlined
121
+ dense
122
+ />
123
+ </div>
124
+ </div>
125
+ </q-card-section>
126
+
127
+ <q-card-actions align="right">
128
+ <q-btn flat label="Cancel" @click="onDialogCancel" />
129
+ <q-btn color="primary" label="Insert" :disable="!canSubmit" @click="onConfirm" />
130
+ </q-card-actions>
131
+ </q-card>
132
+ </q-dialog>
133
+ </template>
@@ -0,0 +1,20 @@
1
+ interface DialogInsertEditImageProps {
2
+ initialSource?: string;
3
+ initialAlt?: string;
4
+ initialWidth?: number | null;
5
+ initialHeight?: number | null;
6
+ }
7
+ declare const __VLS_export: import("vue").DefineComponent<DialogInsertEditImageProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ hide: (...args: any[]) => void;
9
+ ok: (...args: any[]) => void;
10
+ }, string, import("vue").PublicProps, Readonly<DialogInsertEditImageProps> & Readonly<{
11
+ onHide?: ((...args: any[]) => any) | undefined;
12
+ onOk?: ((...args: any[]) => any) | undefined;
13
+ }>, {
14
+ initialSource: string;
15
+ initialAlt: string;
16
+ initialWidth: number | null;
17
+ initialHeight: number | null;
18
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -0,0 +1,20 @@
1
+ interface DialogInsertEditLinkProps {
2
+ initialUrl?: string;
3
+ initialText?: string;
4
+ initialTitle?: string;
5
+ initialTarget?: '_self' | '_blank';
6
+ }
7
+ declare const __VLS_export: import("vue").DefineComponent<DialogInsertEditLinkProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ hide: (...args: any[]) => void;
9
+ ok: (...args: any[]) => void;
10
+ }, string, import("vue").PublicProps, Readonly<DialogInsertEditLinkProps> & Readonly<{
11
+ onHide?: ((...args: any[]) => any) | undefined;
12
+ onOk?: ((...args: any[]) => any) | undefined;
13
+ }>, {
14
+ initialUrl: string;
15
+ initialText: string;
16
+ initialTitle: string;
17
+ initialTarget: "_self" | "_blank";
18
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -0,0 +1,64 @@
1
+ <script setup>
2
+ import { computed, ref } from "vue";
3
+ import { useDialogPluginComponent } from "quasar";
4
+ const props = defineProps({
5
+ initialUrl: { type: String, required: false, default: "" },
6
+ initialText: { type: String, required: false, default: "" },
7
+ initialTitle: { type: String, required: false, default: "" },
8
+ initialTarget: { type: String, required: false, default: "_self" }
9
+ });
10
+ defineEmits([
11
+ ...useDialogPluginComponent.emits
12
+ ]);
13
+ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
14
+ const url = ref(props.initialUrl);
15
+ const text = ref(props.initialText);
16
+ const title = ref(props.initialTitle);
17
+ const target = ref(props.initialTarget);
18
+ const targetOptions = [
19
+ { label: "Current window", value: "_self" },
20
+ { label: "New window", value: "_blank" }
21
+ ];
22
+ const canSubmit = computed(() => url.value.trim().length > 0);
23
+ const onConfirm = () => {
24
+ if (!canSubmit.value) return;
25
+ onDialogOK({
26
+ url: url.value.trim(),
27
+ text: text.value.trim(),
28
+ title: title.value.trim(),
29
+ target: target.value
30
+ });
31
+ };
32
+ </script>
33
+
34
+ <template>
35
+ <q-dialog ref="dialogRef" @hide="onDialogHide">
36
+ <q-card style="min-width: 520px; max-width: 90vw;">
37
+ <q-card-section class="text-h6">
38
+ Insert/Edit Link
39
+ </q-card-section>
40
+
41
+ <q-card-section class="q-gutter-md">
42
+ <q-input v-model="url" label="URL" outlined dense />
43
+ <q-input v-model="text" label="Text to display" outlined dense />
44
+ <q-input v-model="title" label="Title" outlined dense />
45
+ <q-select
46
+ v-model="target"
47
+ label="Open link in..."
48
+ :options="targetOptions"
49
+ option-label="label"
50
+ option-value="value"
51
+ emit-value
52
+ map-options
53
+ outlined
54
+ dense
55
+ />
56
+ </q-card-section>
57
+
58
+ <q-card-actions align="right">
59
+ <q-btn flat label="Cancel" @click="onDialogCancel" />
60
+ <q-btn color="primary" label="Insert" :disable="!canSubmit" @click="onConfirm" />
61
+ </q-card-actions>
62
+ </q-card>
63
+ </q-dialog>
64
+ </template>
@@ -0,0 +1,20 @@
1
+ interface DialogInsertEditLinkProps {
2
+ initialUrl?: string;
3
+ initialText?: string;
4
+ initialTitle?: string;
5
+ initialTarget?: '_self' | '_blank';
6
+ }
7
+ declare const __VLS_export: import("vue").DefineComponent<DialogInsertEditLinkProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ hide: (...args: any[]) => void;
9
+ ok: (...args: any[]) => void;
10
+ }, string, import("vue").PublicProps, Readonly<DialogInsertEditLinkProps> & Readonly<{
11
+ onHide?: ((...args: any[]) => any) | undefined;
12
+ onOk?: ((...args: any[]) => any) | undefined;
13
+ }>, {
14
+ initialUrl: string;
15
+ initialText: string;
16
+ initialTitle: string;
17
+ initialTarget: "_self" | "_blank";
18
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -0,0 +1,9 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
2
+ hide: (...args: any[]) => void;
3
+ ok: (...args: any[]) => void;
4
+ }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
5
+ onHide?: ((...args: any[]) => any) | undefined;
6
+ onOk?: ((...args: any[]) => any) | undefined;
7
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
8
+ declare const _default: typeof __VLS_export;
9
+ export default _default;
@@ -0,0 +1,25 @@
1
+ <script setup>
2
+ import { useDialogPluginComponent } from "quasar";
3
+ defineEmits([
4
+ ...useDialogPluginComponent.emits
5
+ ]);
6
+ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
7
+ const onInput = (value, row) => {
8
+ onDialogOK({ path: value, row });
9
+ };
10
+ </script>
11
+
12
+ <template>
13
+ <q-dialog ref="dialogRef" @hide="onDialogHide" full-height full-width>
14
+ <q-card style="width: 100%; height: 100%;">
15
+ <Suspense>
16
+ <l-file-manager closable @close="onDialogCancel" @input="onInput"></l-file-manager>
17
+ <template #fallback>
18
+ <div class="flex flex-center" style="height: 100%;">
19
+ <q-spinner color="primary" size="3em" />
20
+ </div>
21
+ </template>
22
+ </Suspense>
23
+ </q-card>
24
+ </q-dialog>
25
+ </template>
@@ -0,0 +1,9 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
2
+ hide: (...args: any[]) => void;
3
+ ok: (...args: any[]) => void;
4
+ }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
5
+ onHide?: ((...args: any[]) => any) | undefined;
6
+ onOk?: ((...args: any[]) => any) | undefined;
7
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
8
+ declare const _default: typeof __VLS_export;
9
+ export default _default;
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, ref, useAttrs } from "vue";
2
+ import { computed, defineAsyncComponent, ref, useAttrs } from "vue";
3
3
  import { useQuasar } from "quasar";
4
4
  import { sanitizeHtml } from "../../composables/sanitizeHtml";
5
5
  const $q = useQuasar();
@@ -63,6 +63,197 @@ const insertTableCMD = () => {
63
63
  edit.runCmd("insertHTML", tableHTML);
64
64
  edit.focus();
65
65
  };
66
+ const toAbsoluteUrl = (value) => {
67
+ const url = value?.trim();
68
+ if (!url) return "";
69
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//") || url.startsWith("data:") || url.startsWith("blob:")) {
70
+ return url;
71
+ }
72
+ return url.startsWith("/") ? `${window.location.origin}${url}` : `${window.location.origin}/${url}`;
73
+ };
74
+ const buildLinkHtml = (payload) => {
75
+ const anchor = document.createElement("a");
76
+ anchor.href = payload.url.trim();
77
+ anchor.textContent = payload.text?.trim() || payload.url.trim();
78
+ if (payload.title?.trim()) anchor.title = payload.title.trim();
79
+ if (payload.target === "_blank") {
80
+ anchor.target = "_blank";
81
+ anchor.rel = "noopener noreferrer";
82
+ }
83
+ return anchor.outerHTML;
84
+ };
85
+ const buildImageHtml = (payload) => {
86
+ const image = document.createElement("img");
87
+ image.setAttribute("src", payload.source.trim());
88
+ if (payload.alt?.trim()) image.setAttribute("alt", payload.alt.trim());
89
+ const width = Number(payload.width);
90
+ if (Number.isFinite(width) && width > 0) {
91
+ image.setAttribute("width", String(Math.round(width)));
92
+ }
93
+ const height = Number(payload.height);
94
+ if (Number.isFinite(height) && height > 0) {
95
+ image.setAttribute("height", String(Math.round(height)));
96
+ }
97
+ return image.outerHTML;
98
+ };
99
+ const selectedImageNode = ref(null);
100
+ const selectedLinkNode = ref(null);
101
+ const updateSelectionState = (e) => {
102
+ let target = e?.target;
103
+ if (target instanceof HTMLImageElement) {
104
+ selectedImageNode.value = target;
105
+ selectedLinkNode.value = target.closest("a");
106
+ return;
107
+ }
108
+ if (target && target.closest) {
109
+ const anchor = target.closest("a");
110
+ if (anchor) {
111
+ selectedLinkNode.value = anchor;
112
+ selectedImageNode.value = null;
113
+ return;
114
+ }
115
+ }
116
+ const selection = window.getSelection();
117
+ if (!selection || !selection.rangeCount) {
118
+ selectedLinkNode.value = null;
119
+ selectedImageNode.value = null;
120
+ return;
121
+ }
122
+ let node = selection.focusNode;
123
+ if (!node) return;
124
+ let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
125
+ selectedLinkNode.value = el?.closest("a");
126
+ if (el instanceof HTMLImageElement) {
127
+ selectedImageNode.value = el;
128
+ } else if (el && selection.anchorNode === selection.focusNode) {
129
+ const offset = selection.focusOffset;
130
+ if (el.childNodes.length > offset) {
131
+ const child = el.childNodes[offset];
132
+ if (child instanceof HTMLImageElement) {
133
+ selectedImageNode.value = child;
134
+ return;
135
+ }
136
+ }
137
+ const anchorOffset = selection.anchorOffset;
138
+ if (el.childNodes.length > anchorOffset) {
139
+ const child = el.childNodes[anchorOffset];
140
+ if (child instanceof HTMLImageElement) {
141
+ selectedImageNode.value = child;
142
+ return;
143
+ }
144
+ }
145
+ selectedImageNode.value = null;
146
+ } else {
147
+ selectedImageNode.value = null;
148
+ }
149
+ };
150
+ const onEditorClick = (e) => {
151
+ updateSelectionState(e);
152
+ };
153
+ const onEditorDblClick = (e) => {
154
+ updateSelectionState(e);
155
+ if (selectedImageNode.value) {
156
+ insertEditImageCMD();
157
+ } else if (selectedLinkNode.value) {
158
+ insertEditLinkCMD();
159
+ }
160
+ };
161
+ const openInsertEditImageDialog = (initialSource = "", skipSaveCaret = false) => {
162
+ const edit = editorRef.value;
163
+ if (!skipSaveCaret) {
164
+ edit.caret.save();
165
+ }
166
+ let initialAlt = "";
167
+ let initialWidth = null;
168
+ let initialHeight = null;
169
+ if (selectedImageNode.value && !initialSource) {
170
+ initialSource = selectedImageNode.value.getAttribute("src") || "";
171
+ initialAlt = selectedImageNode.value.getAttribute("alt") || "";
172
+ initialWidth = selectedImageNode.value.getAttribute("width") || selectedImageNode.value.clientWidth;
173
+ initialHeight = selectedImageNode.value.getAttribute("height") || selectedImageNode.value.clientHeight;
174
+ }
175
+ $q.dialog({
176
+ component: defineAsyncComponent(() => import("./DialogInsertEditImage.vue")),
177
+ componentProps: { initialSource, initialAlt, initialWidth, initialHeight }
178
+ }).onOk((payload) => {
179
+ if (!payload?.source?.trim()) return;
180
+ setTimeout(() => {
181
+ edit.focus();
182
+ edit.caret.restore();
183
+ if (selectedImageNode.value && !skipSaveCaret) {
184
+ selectedImageNode.value.setAttribute("src", payload.source.trim());
185
+ if (payload.alt?.trim()) selectedImageNode.value.setAttribute("alt", payload.alt.trim());
186
+ else selectedImageNode.value.removeAttribute("alt");
187
+ if (payload.width) selectedImageNode.value.setAttribute("width", String(Math.round(payload.width)));
188
+ else selectedImageNode.value.removeAttribute("width");
189
+ if (payload.height) selectedImageNode.value.setAttribute("height", String(Math.round(payload.height)));
190
+ else selectedImageNode.value.removeAttribute("height");
191
+ emit("update:modelValue", edit.$el.querySelector(".q-editor__content").innerHTML);
192
+ } else {
193
+ edit.runCmd("insertHTML", buildImageHtml(payload));
194
+ }
195
+ }, 100);
196
+ });
197
+ };
198
+ const insertEditLinkCMD = () => {
199
+ const edit = editorRef.value;
200
+ edit.caret.save();
201
+ let initialUrl = "";
202
+ let initialText = window.getSelection()?.toString() || "";
203
+ let initialTitle = "";
204
+ let initialTarget = "_self";
205
+ if (selectedLinkNode.value) {
206
+ initialUrl = selectedLinkNode.value.getAttribute("href") || "";
207
+ initialText = selectedLinkNode.value.textContent || initialText;
208
+ initialTitle = selectedLinkNode.value.getAttribute("title") || "";
209
+ initialTarget = selectedLinkNode.value.getAttribute("target") === "_blank" ? "_blank" : "_self";
210
+ }
211
+ $q.dialog({
212
+ component: defineAsyncComponent(() => import("./DialogInsertEditLink.vue")),
213
+ componentProps: { initialUrl, initialText, initialTitle, initialTarget }
214
+ }).onOk((payload) => {
215
+ if (!payload?.url?.trim()) return;
216
+ setTimeout(() => {
217
+ edit.focus();
218
+ edit.caret.restore();
219
+ if (selectedLinkNode.value) {
220
+ selectedLinkNode.value.setAttribute("href", payload.url.trim());
221
+ selectedLinkNode.value.textContent = payload.text?.trim() || payload.url.trim();
222
+ if (payload.title?.trim()) selectedLinkNode.value.setAttribute("title", payload.title.trim());
223
+ else selectedLinkNode.value.removeAttribute("title");
224
+ if (payload.target === "_blank") {
225
+ selectedLinkNode.value.setAttribute("target", "_blank");
226
+ selectedLinkNode.value.setAttribute("rel", "noopener noreferrer");
227
+ } else {
228
+ selectedLinkNode.value.removeAttribute("target");
229
+ selectedLinkNode.value.removeAttribute("rel");
230
+ }
231
+ emit("update:modelValue", edit.$el.querySelector(".q-editor__content").innerHTML);
232
+ } else {
233
+ edit.runCmd("insertHTML", buildLinkHtml(payload));
234
+ }
235
+ }, 100);
236
+ });
237
+ };
238
+ const insertEditImageCMD = () => {
239
+ openInsertEditImageDialog();
240
+ };
241
+ const insertImageFromFileManagerCMD = () => {
242
+ const edit = editorRef.value;
243
+ edit.caret.save();
244
+ $q.dialog({
245
+ component: defineAsyncComponent(() => import("./DialogSelectFileFromFileManager.vue"))
246
+ }).onOk((result) => {
247
+ if (!result?.path?.trim()) return;
248
+ let url = "";
249
+ if (result.row && result.row.publicUrl) {
250
+ url = result.row.publicUrl;
251
+ } else {
252
+ url = toAbsoluteUrl(result.path);
253
+ }
254
+ openInsertEditImageDialog(url, true);
255
+ });
256
+ };
66
257
  const emit = defineEmits(["update:modelValue"]);
67
258
  const props = defineProps({
68
259
  fullscreen: { type: Boolean, required: false, skipCheck: true },
@@ -109,6 +300,10 @@ const attrs = computed(() => {
109
300
  const a = { ...props };
110
301
  if (props.toolbar === void 0) a.toolbar = newToolBar;
111
302
  if (props.fonts === void 0) a.fonts = newFonts;
303
+ a.definitions = {
304
+ ...newDefinitions,
305
+ ...props.definitions
306
+ };
112
307
  return a;
113
308
  });
114
309
  const newToolBar = [
@@ -128,7 +323,7 @@ const newToolBar = [
128
323
  }
129
324
  ],
130
325
  ["bold", "italic", "strike", "underline", "subscript", "superscript"],
131
- ["token", "hr", "link"],
326
+ ["token", "hr", "insertEditLink", "insertEditImage", "insertImageFromFileManager"],
132
327
  //['print'],
133
328
  ["fullscreen"],
134
329
  [
@@ -185,10 +380,49 @@ const newFonts = {
185
380
  times_new_roman: "Times New Roman",
186
381
  verdana: "Verdana"
187
382
  };
383
+ const newDefinitions = {
384
+ insertEditLink: {
385
+ tip: "Insert/Edit Link",
386
+ icon: "sym_o_link",
387
+ handler: insertEditLinkCMD
388
+ },
389
+ insertEditImage: {
390
+ tip: "Insert/Edit Image",
391
+ icon: "sym_o_add_photo_alternate",
392
+ handler: insertEditImageCMD
393
+ },
394
+ insertImageFromFileManager: {
395
+ tip: "Insert image from FileManager",
396
+ icon: "sym_o_folder_open",
397
+ handler: insertImageFromFileManagerCMD
398
+ }
399
+ };
188
400
  </script>
189
401
 
190
402
  <template>
191
- <q-editor v-model="localValue" ref="editorRef" v-bind="attrs" @blur="onEditorBlur">
403
+ <q-editor v-model="localValue" ref="editorRef" v-bind="attrs" @blur="onEditorBlur" @click="onEditorClick" @keyup="onEditorClick" @dblclick="onEditorDblClick">
404
+ <template v-slot:insertEditImage>
405
+ <q-btn dense no-caps no-wrap
406
+ :flat="!selectedImageNode"
407
+ :unelevated="!!selectedImageNode"
408
+ :color="selectedImageNode ? 'primary' : 'grey-8'"
409
+ icon="sym_o_add_photo_alternate"
410
+ size="sm"
411
+ @click="insertEditImageCMD">
412
+ <q-tooltip>Insert/Edit Image</q-tooltip>
413
+ </q-btn>
414
+ </template>
415
+ <template v-slot:insertEditLink>
416
+ <q-btn dense no-caps no-wrap
417
+ :flat="!selectedLinkNode"
418
+ :unelevated="!!selectedLinkNode"
419
+ :color="selectedLinkNode ? 'primary' : 'grey-8'"
420
+ icon="sym_o_link"
421
+ size="sm"
422
+ @click="insertEditLinkCMD">
423
+ <q-tooltip>Insert/Edit Link</q-tooltip>
424
+ </q-btn>
425
+ </template>
192
426
  <template v-slot:fontSize>
193
427
  <q-btn-dropdown dense no-caps ref="fontSizeRef" no-wrap unelevated color="white" text-color="default"
194
428
  label="Font Size" icon="sym_o_format_size" size="sm">
@@ -210,18 +444,6 @@ const newFonts = {
210
444
  </q-item-section>
211
445
  <q-item-section>
212
446
  <q-color v-model="foreColor" no-header no-footer style="max-width: 250px;" />
213
- <!-- :palette="[
214
- '#ff0000',
215
- '#ff8000',
216
- '#ffff00',
217
- '#00ff00',
218
- '#00ff80',
219
- '#00ffff',
220
- '#0080ff',
221
- '#0000ff',
222
- '#8000ff',
223
- '#ff00ff'
224
- ]" -->
225
447
  </q-item-section>
226
448
  </q-item>
227
449
  </q-btn-dropdown>
@@ -167,7 +167,7 @@ const onDblclickRow = (evt, row, index) => {
167
167
  return;
168
168
  }
169
169
  if (row.__typename == "File") {
170
- emit("input", row.path);
170
+ emit("input", row.path, row);
171
171
  }
172
172
  };
173
173
  const findFolder = (path, folders2) => {
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { q } from "#imports";
2
3
  const emits = defineEmits(["submit"]);
3
4
  defineProps({
4
5
  mail_driver: { type: String, required: true, default: "mail" },
@@ -12,12 +13,6 @@ defineProps({
12
13
  mail_reply_to: { type: String, required: true },
13
14
  mail_reply_to_name: { type: String, required: true }
14
15
  });
15
- const onLoginGmail = () => {
16
- let state = encodeURIComponent(window.self.location.origin + window.self.location.pathname + "?mail_driver=gmail");
17
- let scope = encodeURIComponent("https://mail.google.com/");
18
- let url = "https://accounts.google.com/o/oauth2/v2/auth?scope=" + scope + "&access_type=offline&state=" + state + "&response_type=code&redirect_uri=https%3A%2F%2Fraymond4.hostlink.com.hk%2Flight%2Fgmail_notification%2Fredirect.php&client_id=790028313082-8qqnoqvkqtqssufto11k6qe6pnievcpv.apps.googleusercontent.com&prompt=consent";
19
- window.open(url, "_blank");
20
- };
21
16
  </script>
22
17
 
23
18
  <template>
@@ -35,7 +30,19 @@ const onLoginGmail = () => {
35
30
  ]" validation="required"></FormKit>
36
31
 
37
32
  <template v-if="value.mail_driver === 'gmail'">
38
- <l-btn label="Login" @click="onLoginGmail" icon="sym_o_login" />
33
+ <div class="col-12">
34
+ <q-banner class="bg-info text-white" rounded>
35
+ <div class="text-weight-bold q-mb-xs">How to create an App Password</div>
36
+ <ol class="q-pl-md q-my-none">
37
+ <li>Enable 2-Step Verification in your Google Account Security settings.</li>
38
+ <li>Go to the <a href="https://myaccount.google.com/apppasswords" target="_blank" class="text-white" style="text-decoration: underline;">Google App Passwords page</a>.</li>
39
+ <li>Select app and device, or choose Other (Custom name), then click Generate.</li>
40
+ <li>Copy the 16-character password and paste it below.</li>
41
+ </ol>
42
+ </q-banner>
43
+ </div>
44
+ <FormKit type="l-input" label="Gmail Address" name="mail_username" validation="required|email"></FormKit>
45
+ <FormKit type="l-input" label="App Password" name="mail_password" validation="required" :attrs="{ type: 'password' }"></FormKit>
39
46
  </template>
40
47
 
41
48
  <template v-else>
@@ -50,7 +57,7 @@ const onLoginGmail = () => {
50
57
  <FormKit type="l-input" label="SMTP Host" name="mail_host"></FormKit>
51
58
  <FormKit type="l-input" label="SMTP Port" name="mail_port"></FormKit>
52
59
  <FormKit type="l-input" label="SMTP Username" name="mail_username"></FormKit>
53
- <FormKit type="l-input" label="SMTP Password" name="mail_password"></FormKit>
60
+ <FormKit type="l-input" label="SMTP Password" name="mail_password" :attrs="{ type: 'password' }"></FormKit>
54
61
 
55
62
  <FormKit type="l-select" label="SMTP Encryption" name="mail_encryption" :options="[
56
63
  { label: 'None', value: '' },
@@ -1,5 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  file_manager: string;
3
+ notification_bell_enabled: string;
3
4
  };
4
5
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
6
  submit: (...args: any[]) => void;
@@ -1,7 +1,8 @@
1
1
  <script setup>
2
2
  const emits = defineEmits(["submit"]);
3
3
  defineProps({
4
- file_manager: { type: String, required: true }
4
+ file_manager: { type: String, required: true },
5
+ notification_bell_enabled: { type: String, required: true }
5
6
  });
6
7
  </script>
7
8
 
@@ -10,6 +11,9 @@ defineProps({
10
11
  <l-field :label="$t('File Manager')" stack-label hide-bottom-space class="col-6">
11
12
  <form-kit type="l-checkbox" label="Show" name="file_manager" true-value="1" false-value="0" />
12
13
  </l-field>
13
-
14
+
15
+ <l-field :label="$t('Notification Bell')" stack-label hide-bottom-space class="col-6">
16
+ <form-kit type="l-checkbox" label="Show" name="notification_bell_enabled" true-value="1" false-value="0" />
17
+ </l-field>
14
18
  </form-kit>
15
19
  </template>
@@ -1,5 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  file_manager: string;
3
+ notification_bell_enabled: string;
3
4
  };
4
5
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
6
  submit: (...args: any[]) => void;
@@ -49,7 +49,7 @@ let columns = [
49
49
  const onCickView = async (id) => {
50
50
  try {
51
51
  if (await m("viewAs", { user_id: id })) {
52
- window.location.reload();
52
+ window.location.href = "/";
53
53
  }
54
54
  } catch (e) {
55
55
  light.dialog({
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,26 @@
1
+ <script setup>
2
+ import { useRoute, navigateTo } from "#imports";
3
+ const route = useRoute();
4
+ const attemptedPath = route.query.path || route.fullPath;
5
+ </script>
6
+
7
+ <template>
8
+ <l-page class="q-pa-md">
9
+ <q-banner class="bg-negative text-white">
10
+ <template v-slot:avatar>
11
+ <q-icon name="sym_o_error" />
12
+ </template>
13
+ <div class="text-h6">Page not found</div>
14
+ <div v-if="attemptedPath && attemptedPath !== '/'">
15
+ The requested page was not found or you do not have permission to access it:
16
+ <code>{{ attemptedPath }}</code>
17
+ </div>
18
+ <div v-else>
19
+ The requested page was not found or you do not have permission to access it.
20
+ </div>
21
+ </q-banner>
22
+ <div class="q-mt-md">
23
+ <q-btn color="primary" icon="sym_o_home" label="Home" to="/" />
24
+ </div>
25
+ </l-page>
26
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostlink/nuxt-light",
3
- "version": "1.68.0",
3
+ "version": "1.70.1",
4
4
  "description": "HostLink Nuxt Light Framework",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,7 +41,7 @@
41
41
  "@hostlink/light": "^3.2.7",
42
42
  "@nuxt/module-builder": "^1.0.1",
43
43
  "@quasar/extras": "^1.17.0",
44
- "@quasar/quasar-ui-qmarkdown": "^2.0.5",
44
+ "@quasar/quasar-ui-qmarkdown": "^3.0.0-rc.1",
45
45
  "@vueuse/core": "^14.0.0",
46
46
  "axios": "^1.12.2",
47
47
  "defu": "^6.1.4",
@@ -65,6 +65,7 @@
65
65
  "eslint": "^9.39.1",
66
66
  "nuxt": "^4.3.0",
67
67
  "typescript": "^5.9.2",
68
+ "vitest": "^3.2.6",
68
69
  "vue-tsc": "^3.1.5"
69
70
  }
70
71
  }