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.
- package/.vscode/extensions.json +3 -0
- package/README.md +45 -0
- package/index.html +13 -0
- package/package.json +39 -0
- package/public/vite.svg +1 -0
- package/src/assets/vue.svg +1 -0
- package/src/components/BT-Btn.vue +40 -0
- package/src/components/BT-Col.vue +36 -0
- package/src/components/BT-Span.vue +21 -0
- package/src/components/Dialog-Confirm.vue +47 -0
- package/src/components/Dialog-Select-Date.vue +89 -0
- package/src/components/Dialog-Select.vue +140 -0
- package/src/components/Dialog-Text.vue +59 -0
- package/src/composables/actions-tracker.ts +99 -0
- package/src/composables/actions.ts +354 -0
- package/src/composables/api.ts +471 -0
- package/src/composables/auth.ts +382 -0
- package/src/composables/cosmetics.ts +178 -0
- package/src/composables/csv.ts +198 -0
- package/src/composables/dates.ts +79 -0
- package/src/composables/demo.ts +25 -0
- package/src/composables/dialogs.ts +115 -0
- package/src/composables/document-meta.ts +40 -0
- package/src/composables/draggable.ts +189 -0
- package/src/composables/filters.ts +256 -0
- package/src/composables/forage.ts +49 -0
- package/src/composables/helpers.ts +694 -0
- package/src/composables/id.ts +20 -0
- package/src/composables/list.ts +601 -0
- package/src/composables/navigation.ts +214 -0
- package/src/composables/presets.ts +20 -0
- package/src/composables/pwa.ts +89 -0
- package/src/composables/resizable.ts +382 -0
- package/src/composables/rules.ts +40 -0
- package/src/composables/stores.ts +797 -0
- package/src/composables/track.ts +55 -0
- package/src/composables/urls.ts +11 -0
- package/src/core.ts +92 -0
- package/src/index.ts +16 -0
- package/src/types.ts +13 -0
- package/src/useApi.ts +68 -0
- package/src/vite-env.d.ts +1 -0
- package/test/api.test.ts +84 -0
- package/test/forage.test.ts +31 -0
- package/test/helpers.test.ts +231 -0
- package/test/navigation.test.ts +99 -0
- package/test/stores-last-update.test.ts +138 -0
- package/test/stores-session.test.ts +118 -0
- package/test/track.test.ts +29 -0
- package/test/utils.ts +15 -0
- package/tsconfig.json +33 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +19 -0
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
|
+
}
|
package/public/vite.svg
ADDED
|
@@ -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
|
+
}
|