bt-core-app 0.0.0

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 (53) hide show
  1. package/.vscode/extensions.json +3 -0
  2. package/README.md +45 -0
  3. package/index.html +13 -0
  4. package/package.json +39 -0
  5. package/public/vite.svg +1 -0
  6. package/src/assets/vue.svg +1 -0
  7. package/src/components/BT-Btn.vue +40 -0
  8. package/src/components/BT-Col.vue +36 -0
  9. package/src/components/BT-Span.vue +21 -0
  10. package/src/components/Dialog-Confirm.vue +47 -0
  11. package/src/components/Dialog-Select-Date.vue +89 -0
  12. package/src/components/Dialog-Select.vue +140 -0
  13. package/src/components/Dialog-Text.vue +59 -0
  14. package/src/composables/actions-tracker.ts +99 -0
  15. package/src/composables/actions.ts +354 -0
  16. package/src/composables/api.ts +471 -0
  17. package/src/composables/auth.ts +382 -0
  18. package/src/composables/cosmetics.ts +178 -0
  19. package/src/composables/csv.ts +198 -0
  20. package/src/composables/dates.ts +79 -0
  21. package/src/composables/demo.ts +25 -0
  22. package/src/composables/dialogs.ts +115 -0
  23. package/src/composables/document-meta.ts +40 -0
  24. package/src/composables/draggable.ts +189 -0
  25. package/src/composables/filters.ts +256 -0
  26. package/src/composables/forage.ts +49 -0
  27. package/src/composables/helpers.ts +694 -0
  28. package/src/composables/id.ts +20 -0
  29. package/src/composables/list.ts +601 -0
  30. package/src/composables/navigation.ts +214 -0
  31. package/src/composables/presets.ts +20 -0
  32. package/src/composables/pwa.ts +89 -0
  33. package/src/composables/resizable.ts +382 -0
  34. package/src/composables/rules.ts +40 -0
  35. package/src/composables/stores.ts +797 -0
  36. package/src/composables/track.ts +55 -0
  37. package/src/composables/urls.ts +11 -0
  38. package/src/core.ts +92 -0
  39. package/src/index.ts +16 -0
  40. package/src/types.ts +13 -0
  41. package/src/useApi.ts +68 -0
  42. package/src/vite-env.d.ts +1 -0
  43. package/test/api.test.ts +84 -0
  44. package/test/forage.test.ts +31 -0
  45. package/test/helpers.test.ts +231 -0
  46. package/test/navigation.test.ts +99 -0
  47. package/test/stores-last-update.test.ts +138 -0
  48. package/test/stores-session.test.ts +118 -0
  49. package/test/track.test.ts +29 -0
  50. package/test/utils.ts +15 -0
  51. package/tsconfig.json +33 -0
  52. package/tsconfig.node.json +11 -0
  53. package/vite.config.ts +19 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3
+ }
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ ## Vue 3 + TypeScript + Vite
2
+
3
+ This template should help get you started developing with Vue 3 and TypeScript in Vite. It includes a number of helpful components and composables.
4
+ The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
5
+
6
+ ## Recommended IDE Setup
7
+
8
+ - [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
9
+
10
+
11
+ ## Environment modes - production, staging, development
12
+
13
+ Control what mode you are in by adding environment files to your project root folder with these variables:
14
+
15
+ ```
16
+ VITE_BASE_AUTH_URL=[https://auth...]
17
+ VITE_BASE_DATA_URL=[https://data...]
18
+ VITE_WEB_APP_URL=[https://webapp...]
19
+ VITE_[MICROSERVICE]=[https://microservice...]
20
+ VITE_LOCAL_DB_NAME=[btweb]
21
+ ```
22
+ The VITE_[MICROSERVICE] variable can include any number of variables like this:
23
+
24
+ ```
25
+ VITE_auth=[https://data_auth...]
26
+ VITE_ordering=[https://data_ordering...]
27
+ VITE_courier-invoicing=[https://data_courier-invoicing...]
28
+ ```
29
+
30
+ ## Auth modes
31
+
32
+ Auth credentials are stored locally using @vueuse/core's useStorage composable. You can change the storage key by setting an environment variable:
33
+
34
+ ```
35
+ VITE_AUTH_STORAGE_TOKEN=[string]
36
+ ```
37
+
38
+ Permissions take the format [permission].[access]. For instance:
39
+
40
+ ```
41
+ customers.view
42
+ customers.edit
43
+ ```
44
+
45
+ If the navigation item requires a permission of 'customers', then it will allow viewing and also editing.
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Vite + Vue + TS</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "bt-core-app",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vue-tsc && vite build",
8
+ "preview": "vite preview",
9
+ "test": "vitest",
10
+ "test live": "vitest watch"
11
+ },
12
+ "dependencies": {
13
+ "@vuepic/vue-datepicker": "^8.3.1",
14
+ "localforage": "^1.10.0",
15
+ "luxon": "^3.4.4",
16
+ "thenby": "^1.3.4",
17
+ "vuejs-confirm-dialog": "^0.5.1"
18
+ },
19
+ "peerDependencies": {
20
+ "@vueuse/core": "^10.4.1",
21
+ "pinia": "^2.0.24",
22
+ "vue": "^3.4.21",
23
+ "vue-router": "^4.0.0",
24
+ "vuetify": "^3.4.10"
25
+ },
26
+ "devDependencies": {
27
+ "@testing-library/vue": "^8.0.3",
28
+ "@types/luxon": "^3.4.2",
29
+ "@vitejs/plugin-vue": "^5.0.4",
30
+ "@vitest/ui": "^1.4.0",
31
+ "happy-dom": "^14.3.6",
32
+ "msw": "^2.2.10",
33
+ "typescript": "^5.2.2",
34
+ "vite": "^5.1.6",
35
+ "vitest": "^1.4.0",
36
+ "vue": "^3.4.21",
37
+ "vue-tsc": "^1.8.27"
38
+ }
39
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <v-btn
3
+ v-bind="$attrs"
4
+ :disabled="!mIsEditing"
5
+ :icon="mIcon"
6
+ :size="mSize"
7
+ :variant="mVariant" />
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import { computed, inject, ref } from 'vue'
12
+ import { usePresets } from '../useApi'
13
+
14
+ interface BtnProps {
15
+ icon?: string
16
+ isEditing?: boolean
17
+ preset?: string
18
+ size?: string,
19
+ variant?: 'flat' | 'text' | 'elevated' | 'tonal' | 'outlined' | 'plain'
20
+ }
21
+
22
+ const props = withDefaults(defineProps<BtnProps>(), {
23
+ icon: undefined,
24
+ isEditing: true,
25
+ size: undefined,
26
+ variant: 'text'
27
+ })
28
+
29
+ const presets = usePresets(props.preset)
30
+
31
+ const mIcon = computed(() => props.icon ?? presets.icon)
32
+
33
+ const iIsEditing = inject('isEditing', () => ref(false), true)
34
+ const mIsEditing = computed(() => props.isEditing ?? presets.isEditing ?? iIsEditing.value)
35
+
36
+ const iSize = inject('size', () => ref(props.size ?? 'small'), true)
37
+ const mSize = computed(() => props.size ?? presets.size ?? iSize.value)
38
+
39
+ const mVariant = computed(() => props.variant ?? presets.variant)
40
+ </script>
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <v-col
3
+ :lg="mLg"
4
+ :md="mMd"
5
+ :sm="mSm"
6
+ :cols="cols"
7
+ :class="noGutters ? 'px-0' : ''">
8
+ <slot></slot>
9
+ </v-col>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { computed, inject, ref } from 'vue'
14
+
15
+ interface ColProps {
16
+ cols?: string | boolean
17
+ isMobile?: boolean
18
+ lg?: string | boolean
19
+ md?: string | boolean
20
+ sm?: string | boolean
21
+ noGutters?: boolean
22
+ }
23
+
24
+ const props = withDefaults(defineProps<ColProps>(), {
25
+ cols: '12',
26
+ lg: false,
27
+ md: false,
28
+ sm: '6'
29
+ })
30
+
31
+ const mIsMobile = inject('isMobile', () => ref(false), true)
32
+
33
+ const mLg = computed(() => (props.isMobile ?? mIsMobile.value) ? false : props.lg)
34
+ const mMd = computed(() => (props.isMobile ?? mIsMobile.value) ? false : props.md)
35
+ const mSm = computed(() => (props.isMobile ?? mIsMobile.value) ? false : props.sm)
36
+ </script>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <span>{{ prefix }}{{ displayText }}{{ suffix }}</span>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { useFilters } from '../index'
7
+ import { computed } from 'vue'
8
+
9
+ interface Props {
10
+ filter: string
11
+ prefix?: string
12
+ suffix?: string
13
+ value: any
14
+ }
15
+
16
+ const props = defineProps<Props>()
17
+ const { findFilter } = useFilters()
18
+ const displayText = computed(() => {
19
+ return props.filter ? findFilter(props.filter)(props.value) : props.value
20
+ })
21
+ </script>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <v-dialog v-model="show">
3
+ <div>
4
+ <v-card :max-width="maxWidth ?? '400'" :min-width="minWidth" :title="title" class="mx-auto">
5
+ <v-card-text>
6
+ <div class="text-h6">{{ msg }}</div>
7
+ </v-card-text>
8
+ <v-card-actions>
9
+ <v-btn @click="cancel">{{ cancelText ?? 'No' }}</v-btn>
10
+ <v-btn @click="confirm">{{ confirmText ?? 'Yes' }}</v-btn>
11
+ </v-card-actions>
12
+ </v-card>
13
+ </div>
14
+ </v-dialog>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { onMounted } from 'vue';
19
+ import { type ConfirmDialogProps } from '../composables/dialogs'
20
+
21
+ const props = withDefaults(defineProps<ConfirmDialogProps>(), {
22
+ cancelValue: false,
23
+ confirmValue: true,
24
+ startIncludeDetails: false
25
+ })
26
+
27
+ // const props = defineProps(['cancelText', 'confirmText', 'msg', 'minWidth', 'title'])
28
+
29
+ const emit = defineEmits(['confirm', 'cancel'])
30
+
31
+ let show = false
32
+
33
+ function cancel() {
34
+ show = false
35
+ emit('cancel', props.cancelValue)
36
+ }
37
+
38
+ function confirm() {
39
+ show = false
40
+ emit('confirm', props.confirmValue)
41
+ }
42
+
43
+ onMounted(() => {
44
+ show = true
45
+ })
46
+
47
+ </script>
@@ -0,0 +1,89 @@
1
+ <template>
2
+ <v-dialog v-model="show">
3
+ <div>
4
+ <v-card
5
+ :max-width="maxWidth ?? '400'"
6
+ :min-width="minWidth"
7
+ class="mx-auto"
8
+ :title="title">
9
+ <v-card-text class="pa-0">
10
+ <VueDatePicker
11
+ auto-apply
12
+ dark
13
+ :enable-time-picker="useTime"
14
+ inline
15
+ :is-24="false"
16
+ :model-auto="range"
17
+ :range="range"
18
+ :time-picker-inline="!range && useTime"
19
+ :timezone="credentials.timeZone"
20
+ utc
21
+ v-bind="$attrs"
22
+ v-model="date" />
23
+ </v-card-text>
24
+ <v-card-actions>
25
+ <v-row no-gutters>
26
+ <v-col cols="6">
27
+ <v-btn v-if="required !== true" block @click="cancel">{{ cancelText ?? 'Cancel' }}</v-btn>
28
+ </v-col>
29
+ <v-col cols="6">
30
+ <v-btn block @click="accept" :disabled="!canAccept">{{ confirmText ?? 'OK' }}</v-btn>
31
+ </v-col>
32
+ </v-row>
33
+ </v-card-actions>
34
+ </v-card>
35
+ </div>
36
+ </v-dialog>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import { computed, onMounted, ref } from 'vue'
41
+ import VueDatePicker from '@vuepic/vue-datepicker'
42
+ import '@vuepic/vue-datepicker/dist/main.css'
43
+ import { useAuth } from '../useApi'
44
+ import { type SelectDateProps } from '../composables/dialogs'
45
+
46
+ const props = withDefaults(defineProps<SelectDateProps>(), {
47
+ cancelValue: false,
48
+ height: '400'
49
+ })
50
+
51
+ const emit = defineEmits(['confirm', 'cancel'])
52
+ const date = ref()
53
+ const { credentials } = useAuth()
54
+
55
+ const canAccept = computed(() => {
56
+ return !props.required || date.value
57
+ })
58
+
59
+ let show = false
60
+
61
+ function accept() {
62
+ if (props.required && !date.value) {
63
+ //required
64
+ return
65
+ }
66
+
67
+ show = false
68
+ emit('confirm', date.value)
69
+ }
70
+
71
+ // function maybeAccept(items: any) {
72
+ // if (!props.multiple) {
73
+ // if (isArrayOfLength(items, 1) || (props.canUnselect && !isLengthyArray(items))) {
74
+ // show = false
75
+ // emit('confirm', getSelectedValues(items))
76
+ // }
77
+ // }
78
+ // }
79
+
80
+ function cancel() {
81
+ show = false
82
+ emit('cancel', undefined)
83
+ }
84
+
85
+ onMounted(() => {
86
+ show = true
87
+ })
88
+
89
+ </script>
@@ -0,0 +1,140 @@
1
+ <template>
2
+ <v-dialog v-model="show">
3
+ <div>
4
+ <v-card
5
+ :max-width="maxWidth ?? '400'"
6
+ :min-width="minWidth"
7
+ class="mx-auto"
8
+ :title="title">
9
+ <!-- <v-card-title v-if="title != null">{{ title }}</v-card-title> -->
10
+ <v-card-text class="pa-0">
11
+ <v-text-field
12
+ v-if="ui.showSearch.value"
13
+ append-inner-icon="mdi-magnify"
14
+ density="compact"
15
+ hide-details
16
+ placeholder="Search"
17
+ v-model="ui.searchString.value" />
18
+ <!-- <div v-if="multiple" @click="selectAll" class="text-center my-1">
19
+ <v-btn>Select All</v-btn>
20
+ </div> -->
21
+ <v-list v-if="isLengthyArray(ui.filteredItems.value)"
22
+ class="overflow-y-auto"
23
+ v-model:selected="selection"
24
+ :height="height"
25
+ :select-strategy="multiple ? 'independent' : 'single-independent'"
26
+ @update:selected="maybeAccept">
27
+ <template v-for="(fItem) in ui.filteredItems.value" :key="index">
28
+ <v-list-item :value="fItem">
29
+ <slot v-bind:item="fItem">
30
+ <v-list-item-title v-if="itemText != null || textFilter != null || textFunction != null">
31
+ {{ displayTitle(fItem) }}
32
+ </v-list-item-title>
33
+ <v-list-item-subtitle v-if="itemSubtext != null || subtextFilter != null || subtextFunction != null">
34
+ {{ displaySubtitle(fItem) }}
35
+ </v-list-item-subtitle>
36
+ </slot>
37
+ </v-list-item>
38
+ </template>
39
+ </v-list>
40
+ </v-card-text>
41
+ <v-card-actions>
42
+ <v-row no-gutters>
43
+ <v-col :cols="multiple ? 6 : 12">
44
+ <v-btn v-if="required !== true" block @click="cancel">{{ cancelText ?? 'Cancel' }}</v-btn>
45
+ </v-col>
46
+ <v-col v-if="multiple == true" cols="6">
47
+ <v-btn block @click="accept" :disabled="!canAccept">{{ confirmText ?? 'OK' }}</v-btn>
48
+ </v-col>
49
+
50
+ </v-row>
51
+ </v-card-actions>
52
+ <v-overlay
53
+ v-model="ui.isLoading.value"
54
+ class="align-center justify-center text-center"
55
+ contained
56
+ persistent>
57
+ <v-progress-circular indeterminate />
58
+ <p>{{ ui.loadingMsg }}</p>
59
+ </v-overlay>
60
+ </v-card>
61
+ </div>
62
+ </v-dialog>
63
+ </template>
64
+
65
+ <script setup lang="ts">
66
+ import { computed, onMounted, ref } from 'vue';
67
+ import { isLengthyArray, isArrayOfLength, nestedValue } from '../composables/helpers'
68
+ import { useFilters } from '../index'
69
+ import { useList } from '../composables/list';
70
+ import { type SelectDialogProps } from '../composables/dialogs'
71
+
72
+ const props = withDefaults(defineProps<SelectDialogProps>(), {
73
+ cancelValue: false,
74
+ height: '400',
75
+ showSearch: true
76
+ })
77
+
78
+ const emit = defineEmits(['confirm', 'cancel'])
79
+
80
+ const ui = useList(props)
81
+ const filters = useFilters()
82
+ const selection = ref([])
83
+
84
+ const displaySubtitle = computed(() => (item: any) => {
85
+ let v = props.subtextFunction != null ? props.subtextFunction(item) : item
86
+ v = props.itemSubtext != null ? nestedValue(v, props.itemSubtext) : v
87
+ return props.subtextFilter != null ? filters.findFilter(props.subtextFilter)(v) : v
88
+ })
89
+
90
+ const displayTitle = computed(() => (item: any) => {
91
+ let v = props.textFunction != null ? props.textFunction(item) : item
92
+ v = props.itemText != null ? nestedValue(v, props.itemText) : v
93
+ return props.textFilter != null ? filters.findFilter(props.textFilter)(v) : v
94
+ })
95
+
96
+ const canAccept = computed(() => {
97
+ return !props.required || isLengthyArray(selection.value)
98
+ })
99
+
100
+ let show = false
101
+
102
+ function getSelectedValues(items: any) {
103
+ const res = props.itemValue != null ? items.map((x: any) => nestedValue(x, props.itemValue)) : items
104
+ if (props.multiple) {
105
+ return res
106
+ }
107
+ else {
108
+ return res.length > 0 ? res[0] : null
109
+ }
110
+ }
111
+
112
+ function accept() {
113
+ if (props.required && !isLengthyArray(selection.value)) {
114
+ //required
115
+ return
116
+ }
117
+
118
+ show = false
119
+ emit('confirm', getSelectedValues(selection.value))
120
+ }
121
+
122
+ function maybeAccept(items: any) {
123
+ if (!props.multiple) {
124
+ if (isArrayOfLength(items, 1) || (props.canUnselect && !isLengthyArray(items))) {
125
+ show = false
126
+ emit('confirm', getSelectedValues(items))
127
+ }
128
+ }
129
+ }
130
+
131
+ function cancel() {
132
+ show = false
133
+ emit('cancel', undefined)
134
+ }
135
+
136
+ onMounted(() => {
137
+ show = true
138
+ })
139
+
140
+ </script>
@@ -0,0 +1,59 @@
1
+ <template>
2
+ <v-dialog v-model="show">
3
+ <div>
4
+ <v-card :max-width="maxWidth ?? '400'" :min-width="minWidth" :title="title" class="mx-auto">
5
+ <v-card-text>
6
+ <v-text-field
7
+ :label="label"
8
+ v-model="mVal" />
9
+ <div class="text-h6">{{ msg }}</div>
10
+ </v-card-text>
11
+ <v-card-actions>
12
+ <v-btn @click="cancel">{{ cancelText ?? 'Cancel' }}</v-btn>
13
+ <v-btn @click="confirm" :disabled="!canAccept">{{ confirmText ?? 'Done' }}</v-btn>
14
+ </v-card-actions>
15
+ </v-card>
16
+ </div>
17
+ </v-dialog>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import { computed, onMounted, ref } from 'vue';
22
+ import { type TextDialogProps } from '../composables/dialogs'
23
+
24
+
25
+ const props = withDefaults(defineProps<TextDialogProps>(), {
26
+ height: '400'
27
+ })
28
+
29
+ const emit = defineEmits(['confirm', 'cancel'])
30
+
31
+ const canAccept = computed(() => {
32
+ return !props.required || mVal.value != null
33
+ })
34
+
35
+ let show = false
36
+
37
+ const mVal = ref(props.value)
38
+
39
+ function confirm() {
40
+ if (props.required && mVal.value == null) {
41
+ //required
42
+ return
43
+ }
44
+
45
+ show = false
46
+
47
+ emit('confirm', mVal.value)
48
+ }
49
+
50
+ function cancel() {
51
+ show = false
52
+ emit('cancel', undefined)
53
+ }
54
+
55
+ onMounted(() => {
56
+ show = true
57
+ })
58
+
59
+ </script>
@@ -0,0 +1,99 @@
1
+ import { computed, ref, shallowRef } from 'vue'
2
+ import type { ShallowRef, Ref } from 'vue'
3
+ import { useConfirmAsync } from '@/composables/dialogs'
4
+ import { watchArray } from '@vueuse/core'
5
+ import { useId } from '@/composables/id'
6
+
7
+ interface LoadingMsg {
8
+ id: string,
9
+ msg: string
10
+ }
11
+
12
+ export interface DoActionOptions {
13
+ completionMsg?: string
14
+ confirmationMsg?: string
15
+ errorMsg?: string
16
+ loadingMsg?: string
17
+ requireConfirmation?: boolean,
18
+ throwError?: boolean
19
+ }
20
+
21
+ export function useActionsTracker(useOptions?: DoActionOptions) {
22
+ const loadingMsgs: Ref<LoadingMsg[]> = ref([])
23
+ const mLoadingMsg: ShallowRef<string | undefined> = shallowRef()
24
+ const mErrorMsg: ShallowRef<string | undefined> = shallowRef()
25
+ const mCompletionMsg: ShallowRef<string | undefined> = shallowRef()
26
+ const isLoading = computed(() => mLoadingMsg.value != null)
27
+
28
+ function endLoading(id: string) {
29
+ loadingMsgs.value = loadingMsgs.value.filter(x => x.id != id)
30
+ }
31
+
32
+ function startLoading(msg?: string): string {
33
+ const e = {
34
+ id: useId(),
35
+ msg: msg ?? 'Loading'
36
+ }
37
+ loadingMsgs.value.push(e)
38
+ return e.id
39
+ }
40
+
41
+ function logError(err?: string | Error) {
42
+ if (err != null)
43
+ mErrorMsg.value = err.toString()
44
+ }
45
+
46
+ function clearErrors() {
47
+ mErrorMsg.value = undefined
48
+ }
49
+
50
+ async function doAction(action: any, options?: DoActionOptions) {
51
+ const opt = { ...options }
52
+ opt.completionMsg ??= useOptions?.completionMsg
53
+ opt.confirmationMsg ??= useOptions?.confirmationMsg
54
+ opt.errorMsg ??= useOptions?.errorMsg
55
+ opt.loadingMsg ??= useOptions?.loadingMsg ?? 'Loading'
56
+ opt.requireConfirmation ??= useOptions?.requireConfirmation
57
+
58
+ const id = startLoading(opt.loadingMsg)
59
+ let res: any = null
60
+
61
+ try {
62
+ if (opt.requireConfirmation || opt.confirmationMsg) {
63
+ if (!await useConfirmAsync(opt.confirmationMsg ?? 'Are you sure?'))
64
+ return
65
+ }
66
+
67
+ res = await action()
68
+
69
+ if (opt.completionMsg != null) {
70
+ mCompletionMsg.value = opt.completionMsg
71
+ }
72
+
73
+ return res
74
+ }
75
+ catch (err) {
76
+ if (opt.throwError == true)
77
+ return res
78
+ else
79
+ logError(opt.errorMsg ?? (err as string) ?? 'error')
80
+ }
81
+ finally {
82
+ endLoading(id)
83
+ }
84
+ }
85
+
86
+ watchArray(loadingMsgs, (newList: LoadingMsg[]) => {
87
+ mLoadingMsg.value = newList.length > 0 ? newList[0].msg : undefined
88
+ }, { deep: true })
89
+
90
+ return {
91
+ actionErrorMsg: mErrorMsg,
92
+ actionLoadingMsg: mLoadingMsg,
93
+ actionCompleteMsg: mCompletionMsg,
94
+ clearErrors,
95
+ doAction,
96
+ isLoading,
97
+ logError
98
+ }
99
+ }