@ramathibodi/nuxt-commons 0.0.9 → 0.1.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/README.md +6 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +14 -9
- package/dist/runtime/components/ExportCSV.vue +4 -4
- package/dist/runtime/components/FileBtn.vue +10 -4
- package/dist/runtime/components/ImportCSV.vue +7 -4
- package/dist/runtime/components/SplitterPanel.vue +12 -22
- package/dist/runtime/components/document/TemplateBuilder.vue +217 -0
- package/dist/runtime/components/form/Dialog.vue +138 -0
- package/dist/runtime/components/form/Hidden.vue +32 -0
- package/dist/runtime/components/form/Pad.vue +18 -6
- package/dist/runtime/components/form/Table.vue +264 -0
- package/dist/runtime/components/master/Autocomplete.vue +159 -0
- package/dist/runtime/composables/api.d.ts +9 -0
- package/dist/runtime/composables/api.mjs +64 -0
- package/dist/runtime/composables/document/template.d.ts +2 -0
- package/dist/runtime/composables/document/template.mjs +104 -0
- package/dist/runtime/composables/graphql.d.ts +17 -0
- package/dist/runtime/composables/graphql.mjs +61 -0
- package/dist/runtime/composables/menu.d.ts +19 -0
- package/dist/runtime/composables/menu.mjs +60 -0
- package/dist/runtime/composables/utils/fuzzy.d.ts +2 -0
- package/dist/runtime/composables/utils/fuzzy.mjs +19 -0
- package/dist/runtime/composables/utils/validation.d.ts +2 -2
- package/dist/runtime/composables/utils/validation.mjs +2 -2
- package/dist/runtime/labs/form/TextFieldMask.vue +5 -5
- package/dist/runtime/plugins/permission.d.ts +2 -0
- package/dist/runtime/plugins/permission.mjs +23 -0
- package/dist/runtime/types/menu.d.ts +15 -0
- package/package.json +6 -3
- /package/dist/runtime/components/{Pdf → pdf}/Print.vue +0 -0
- /package/dist/runtime/components/{Pdf → pdf}/View.vue +0 -0
package/README.md
CHANGED
|
@@ -67,6 +67,12 @@ npm run test:watch
|
|
|
67
67
|
npm run release
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
## NPM Login
|
|
71
|
+
```bash
|
|
72
|
+
npm login
|
|
73
|
+
```
|
|
74
|
+
- จะส่ง link url มาให้เข้าไปที่ url login npm
|
|
75
|
+
|
|
70
76
|
## NPM PUBLISH
|
|
71
77
|
```bash
|
|
72
78
|
1. ตรวจสอบ source code, ตรวจสอบ Branch ว่าเป็น master แล้ว จากนั้นตรวจสอบ version ของ npm ว่ามีการเปลี่ยนและไม่ชนกับ version บน npm
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir, addTypeTemplate } from '@nuxt/kit';
|
|
1
|
+
import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir, addPlugin, addTypeTemplate } from '@nuxt/kit';
|
|
2
2
|
|
|
3
3
|
const module = defineNuxtModule({
|
|
4
4
|
meta: {
|
|
@@ -18,15 +18,20 @@ const module = defineNuxtModule({
|
|
|
18
18
|
global: true
|
|
19
19
|
});
|
|
20
20
|
addImportsDir(resolver.resolve("runtime/composables/**"));
|
|
21
|
+
addPlugin({
|
|
22
|
+
src: resolver.resolve("runtime/plugins/permission")
|
|
23
|
+
});
|
|
24
|
+
addTypeTemplate({
|
|
25
|
+
src: resolver.resolve("runtime/types/modules.d.ts"),
|
|
26
|
+
filename: "types/modules.d.ts"
|
|
27
|
+
});
|
|
28
|
+
addTypeTemplate({
|
|
29
|
+
src: resolver.resolve("runtime/types/alert.d.ts"),
|
|
30
|
+
filename: "types/alert.d.ts"
|
|
31
|
+
});
|
|
21
32
|
addTypeTemplate({
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
declare module 'painterro';
|
|
25
|
-
declare module "@zxing/browser"
|
|
26
|
-
declare module "vue-signature-pad"
|
|
27
|
-
declare module "accounting"
|
|
28
|
-
declare module "pdf-vue3"
|
|
29
|
-
`
|
|
33
|
+
src: resolver.resolve("runtime/types/menu.d.ts"),
|
|
34
|
+
filename: "types/menu.d.ts"
|
|
30
35
|
});
|
|
31
36
|
}
|
|
32
37
|
});
|
|
@@ -42,13 +42,13 @@ function exportFile() {
|
|
|
42
42
|
@click="exportFile"
|
|
43
43
|
>
|
|
44
44
|
<template
|
|
45
|
-
v-for="(_,
|
|
45
|
+
v-for="(_, name, index) in ($slots as {})"
|
|
46
46
|
:key="index"
|
|
47
|
-
#[
|
|
47
|
+
#[name]="slotData"
|
|
48
48
|
>
|
|
49
49
|
<slot
|
|
50
|
-
:name="
|
|
51
|
-
v-bind="
|
|
50
|
+
:name="name"
|
|
51
|
+
v-bind="(slotData as object)"
|
|
52
52
|
/>
|
|
53
53
|
</template>
|
|
54
54
|
</VBtn>
|
|
@@ -25,9 +25,15 @@ const openFileInput = () => {
|
|
|
25
25
|
fileInput.value?.click()
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const reset = () => {
|
|
29
|
+
files.value = undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
watch(files, () => {
|
|
29
33
|
emit('update:modelValue', files.value)
|
|
30
34
|
}, { deep: true })
|
|
35
|
+
|
|
36
|
+
defineExpose({ reset })
|
|
31
37
|
</script>
|
|
32
38
|
|
|
33
39
|
<template>
|
|
@@ -36,13 +42,13 @@ watch(files, () => {
|
|
|
36
42
|
@click="openFileInput"
|
|
37
43
|
>
|
|
38
44
|
<template
|
|
39
|
-
v-for="(_,
|
|
45
|
+
v-for="(_, name, index) in ($slots as {})"
|
|
40
46
|
:key="index"
|
|
41
|
-
#[
|
|
47
|
+
#[name]="slotData"
|
|
42
48
|
>
|
|
43
49
|
<slot
|
|
44
|
-
:name="
|
|
45
|
-
v-bind="
|
|
50
|
+
:name="name"
|
|
51
|
+
v-bind="(slotData as object)"
|
|
46
52
|
/>
|
|
47
53
|
</template>
|
|
48
54
|
</v-btn>
|
|
@@ -9,6 +9,7 @@ const emit = defineEmits<{
|
|
|
9
9
|
}>()
|
|
10
10
|
|
|
11
11
|
const loading = ref(false)
|
|
12
|
+
const fileBtnRef = ref()
|
|
12
13
|
|
|
13
14
|
function uploadedFile(files: File[] | File | undefined) {
|
|
14
15
|
if (!files) {
|
|
@@ -34,6 +35,7 @@ function uploadedFile(files: File[] | File | undefined) {
|
|
|
34
35
|
const workbook = XLSX.read(e.target?.result)
|
|
35
36
|
emit('import', XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]))
|
|
36
37
|
loading.value = false
|
|
38
|
+
fileBtnRef.value.reset()
|
|
37
39
|
}
|
|
38
40
|
loading.value = true
|
|
39
41
|
reader.readAsArrayBuffer(files)
|
|
@@ -42,6 +44,7 @@ function uploadedFile(files: File[] | File | undefined) {
|
|
|
42
44
|
|
|
43
45
|
<template>
|
|
44
46
|
<FileBtn
|
|
47
|
+
ref="fileBtnRef"
|
|
45
48
|
v-bind="$attrs"
|
|
46
49
|
color="primary"
|
|
47
50
|
:loading="loading"
|
|
@@ -51,13 +54,13 @@ function uploadedFile(files: File[] | File | undefined) {
|
|
|
51
54
|
@update:model-value="uploadedFile"
|
|
52
55
|
>
|
|
53
56
|
<template
|
|
54
|
-
v-for="(_,
|
|
57
|
+
v-for="(_, name, index) in ($slots as {})"
|
|
55
58
|
:key="index"
|
|
56
|
-
#[
|
|
59
|
+
#[name]="slotData"
|
|
57
60
|
>
|
|
58
61
|
<slot
|
|
59
|
-
:name="
|
|
60
|
-
v-bind="
|
|
62
|
+
:name="name"
|
|
63
|
+
v-bind="(slotData as object)"
|
|
61
64
|
/>
|
|
62
65
|
</template>
|
|
63
66
|
</FileBtn>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref
|
|
2
|
+
import { ref } from 'vue';
|
|
3
3
|
import { VCard } from 'vuetify/components/VCard'
|
|
4
4
|
const isResizing = ref(false);
|
|
5
5
|
const pane1Width = ref('50%');
|
|
6
|
+
const containerRef = ref<HTMLElement>()
|
|
6
7
|
interface Props extends /* @vue-ignore */ InstanceType<typeof VCard['$props']>{
|
|
7
|
-
height : number | string
|
|
8
|
+
height? : number | string
|
|
8
9
|
}
|
|
9
10
|
const props = defineProps<Props>()
|
|
10
11
|
|
|
@@ -17,36 +18,25 @@ const stopResize = () => {
|
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
const resize = (event: MouseEvent) => {
|
|
20
|
-
if (isResizing.value) {
|
|
21
|
-
const
|
|
22
|
-
const containerRect = container.getBoundingClientRect();
|
|
21
|
+
if (isResizing.value && containerRef.value) {
|
|
22
|
+
const containerRect = containerRef.value.getBoundingClientRect();
|
|
23
23
|
const newWidth = event.clientX - containerRect.left;
|
|
24
24
|
pane1Width.value = `${(newWidth / containerRect.width) * 100}%`;
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
|
-
|
|
28
|
-
onMounted(() => {
|
|
29
|
-
document.addEventListener('mousemove', resize);
|
|
30
|
-
document.addEventListener('mouseup', stopResize);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
onUnmounted(() => {
|
|
34
|
-
document.removeEventListener('mousemove', resize);
|
|
35
|
-
document.removeEventListener('mouseup', stopResize);
|
|
36
|
-
});
|
|
37
27
|
</script>
|
|
38
28
|
|
|
39
29
|
<template>
|
|
40
30
|
<v-card :="$attrs">
|
|
41
31
|
<v-sheet border :height="height">
|
|
42
|
-
<div class="
|
|
43
|
-
<
|
|
32
|
+
<div class="d-flex" ref="containerRef" @mouseup="stopResize" @mousemove="resize" >
|
|
33
|
+
<v-sheet :width = "pane1Width">
|
|
44
34
|
<slot name="left"></slot>
|
|
45
|
-
</
|
|
46
|
-
<v-divider :thickness="3" vertical class="cursor-move" @mousedown="startResize"></v-divider>
|
|
47
|
-
<
|
|
35
|
+
</v-sheet>
|
|
36
|
+
<v-divider :thickness="3" vertical class="cursor-move" @mousedown="startResize" ></v-divider>
|
|
37
|
+
<v-sheet :width = "`calc(100% - ${pane1Width})`" >
|
|
48
38
|
<slot name="right"></slot>
|
|
49
|
-
</
|
|
39
|
+
</v-sheet>
|
|
50
40
|
</div>
|
|
51
41
|
</v-sheet>
|
|
52
42
|
|
|
@@ -55,5 +45,5 @@ onUnmounted(() => {
|
|
|
55
45
|
</template>
|
|
56
46
|
|
|
57
47
|
<style scoped>
|
|
58
|
-
|
|
48
|
+
|
|
59
49
|
</style>
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import {computed, ref, watch} from 'vue'
|
|
3
|
+
import * as prettier from 'prettier'
|
|
4
|
+
import prettierPluginHtml from 'prettier/plugins/html'
|
|
5
|
+
import {useDocumentTemplate, validationRulesRegex} from '../../composables/document/template'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
modelValue?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
12
|
+
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits(['update:modelValue'])
|
|
16
|
+
|
|
17
|
+
const isAdvanceMode = computed(() => {
|
|
18
|
+
return props.modelValue && !isValidJsonArrayOfObjects(props.modelValue)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const templateItems = ref<Record<string, any>>([])
|
|
22
|
+
const templateAdvanceCode = ref<string | undefined>()
|
|
23
|
+
|
|
24
|
+
const headers = ref([
|
|
25
|
+
{ title: '',
|
|
26
|
+
key: 'operation',
|
|
27
|
+
width: '100px',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: 'Input Type',
|
|
31
|
+
key: 'inputType',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
title: 'Width',
|
|
35
|
+
key: 'width',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
title: 'Input Label',
|
|
39
|
+
key: 'inputLabel',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: 'Variable Name',
|
|
43
|
+
key: 'variableName',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: 'Configuration',
|
|
47
|
+
key: 'configuration',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
title: 'Action',
|
|
51
|
+
key: 'action',
|
|
52
|
+
width: '120px',
|
|
53
|
+
},
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
function isValidJsonArrayOfObjects(str: string | undefined) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(str as string)
|
|
59
|
+
return Array.isArray(parsed) && parsed.every(item => typeof item === 'object' && item !== null && !Array.isArray(item))
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
watch(templateItems, (newValue) => {
|
|
67
|
+
if (!isAdvanceMode.value) emit('update:modelValue', JSON.stringify(newValue))
|
|
68
|
+
}, { deep: true })
|
|
69
|
+
|
|
70
|
+
watch(templateAdvanceCode, (newValue) => {
|
|
71
|
+
if (isAdvanceMode.value) emit('update:modelValue', newValue)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
watch(() => props.modelValue, (newValue) => {
|
|
75
|
+
if (isValidJsonArrayOfObjects(newValue)) templateItems.value = JSON.parse(newValue as string)
|
|
76
|
+
else templateAdvanceCode.value = newValue
|
|
77
|
+
}, { deep: true, immediate: true })
|
|
78
|
+
|
|
79
|
+
async function convertToAdvanceMode() {
|
|
80
|
+
if (!isAdvanceMode.value) {
|
|
81
|
+
emit('update:modelValue', await prettier.format(useDocumentTemplate(templateItems.value).replaceAll('>', '>\n'), { parser: 'html', plugins: [prettierPluginHtml] }))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const inputTypeChoice = ref(['VTextField', 'VTextarea', 'VSelect','VAutocomplete','FormDate','FormTime','FormDateTime','VCombobox', 'VRadio', 'VRadioInline', 'VCheckbox','VSwitch', 'MasterAutocomplete', 'Header', 'Separator', 'CustomCode'])
|
|
86
|
+
const requireOption = ref(['VSelect','VAutocomplete','VCombobox', 'VRadio', 'VRadioInline', 'MasterAutocomplete'])
|
|
87
|
+
const notRequireVariable = ref(['Header', 'Separator', 'CustomCode'])
|
|
88
|
+
const notRequireLabel = ref(['Separator', 'CustomCode'])
|
|
89
|
+
|
|
90
|
+
const choiceOption = ref(['VSelect', 'VRadio', 'VRadioInline'])
|
|
91
|
+
|
|
92
|
+
const ruleOptions = (inputType: string) => (value: any) => {
|
|
93
|
+
if (choiceOption.value.includes(inputType) && !(/^[^'",]+(,[^'",]+)*$/.test(value))) return 'Invalid options format'
|
|
94
|
+
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<FormTable
|
|
101
|
+
v-if="!isAdvanceMode"
|
|
102
|
+
v-model="templateItems"
|
|
103
|
+
:headers="headers"
|
|
104
|
+
title="Document Template"
|
|
105
|
+
>
|
|
106
|
+
<template #toolbarItems>
|
|
107
|
+
<VBtn
|
|
108
|
+
color="primary"
|
|
109
|
+
variant="flat"
|
|
110
|
+
@click="convertToAdvanceMode()"
|
|
111
|
+
>
|
|
112
|
+
Convert To Advance
|
|
113
|
+
</VBtn>
|
|
114
|
+
</template>
|
|
115
|
+
<template #form="{ data, rules }">
|
|
116
|
+
<v-container fluid>
|
|
117
|
+
<v-row dense>
|
|
118
|
+
<v-col cols="3">
|
|
119
|
+
<v-combobox
|
|
120
|
+
v-model="data.inputType"
|
|
121
|
+
label="Input Type"
|
|
122
|
+
:items="inputTypeChoice"
|
|
123
|
+
:rules="[rules.require()]"
|
|
124
|
+
/>
|
|
125
|
+
</v-col>
|
|
126
|
+
<v-col
|
|
127
|
+
v-if="data.inputType!='Separator'"
|
|
128
|
+
cols="1"
|
|
129
|
+
>
|
|
130
|
+
<v-text-field
|
|
131
|
+
v-model="data.width"
|
|
132
|
+
label="Width"
|
|
133
|
+
:rules="[rules.require()]"
|
|
134
|
+
type="number"
|
|
135
|
+
/>
|
|
136
|
+
</v-col>
|
|
137
|
+
<v-col cols="4">
|
|
138
|
+
<v-text-field
|
|
139
|
+
v-if="!notRequireLabel.includes(data.inputType)"
|
|
140
|
+
v-model="data.inputLabel"
|
|
141
|
+
label="Input Label"
|
|
142
|
+
:rules="[rules.require()]"
|
|
143
|
+
/>
|
|
144
|
+
</v-col>
|
|
145
|
+
<v-col cols="4">
|
|
146
|
+
<v-text-field
|
|
147
|
+
v-if="!notRequireVariable.includes(data.inputType)"
|
|
148
|
+
v-model="data.variableName"
|
|
149
|
+
label="Variable Name"
|
|
150
|
+
:rules="[rules.require()]"
|
|
151
|
+
/>
|
|
152
|
+
</v-col>
|
|
153
|
+
<v-col
|
|
154
|
+
v-if="data.inputType!='CustomCode'"
|
|
155
|
+
cols="12"
|
|
156
|
+
>
|
|
157
|
+
<v-textarea
|
|
158
|
+
v-model="data.inputOptions"
|
|
159
|
+
label="Input Options"
|
|
160
|
+
auto-grow
|
|
161
|
+
:rules="[rules.requireIf(requireOption.includes(data.inputType)), ruleOptions(data.inputType)]"
|
|
162
|
+
/>
|
|
163
|
+
</v-col>
|
|
164
|
+
<v-col v-if="data.inputType!='CustomCode'">
|
|
165
|
+
<v-text-field
|
|
166
|
+
v-model="data.validationRules"
|
|
167
|
+
label="Validation Rules"
|
|
168
|
+
:rules="[rules.regex(validationRulesRegex)]"
|
|
169
|
+
/>
|
|
170
|
+
</v-col>
|
|
171
|
+
<v-col v-if="data.inputType!='CustomCode'">
|
|
172
|
+
<v-text-field
|
|
173
|
+
v-model="data.inputAttributes"
|
|
174
|
+
label="Input Attributes"
|
|
175
|
+
/>
|
|
176
|
+
</v-col>
|
|
177
|
+
<v-col v-if="data.inputType!='Separator'">
|
|
178
|
+
<v-text-field
|
|
179
|
+
v-model="data.columnAttributes"
|
|
180
|
+
label="Column Attributes"
|
|
181
|
+
/>
|
|
182
|
+
</v-col>
|
|
183
|
+
<v-col
|
|
184
|
+
v-if="data.inputType=='CustomCode'"
|
|
185
|
+
cols="12"
|
|
186
|
+
>
|
|
187
|
+
<v-textarea
|
|
188
|
+
v-model="data.inputCustomCode"
|
|
189
|
+
label="Custom Code"
|
|
190
|
+
auto-grow
|
|
191
|
+
:rules="[rules.require()]"
|
|
192
|
+
/>
|
|
193
|
+
</v-col>
|
|
194
|
+
</v-row>
|
|
195
|
+
</v-container>
|
|
196
|
+
</template>
|
|
197
|
+
<template #item.configuration="props">
|
|
198
|
+
<template v-if="props.item.inputType=='CustomCode'">
|
|
199
|
+
<b>Custom Code :</b><br>
|
|
200
|
+
<span style="white-space: pre-line">{{ props.item.inputCustomCode }}</span>
|
|
201
|
+
</template>
|
|
202
|
+
<template v-if="props.item.validationRules">
|
|
203
|
+
<b>Validation Rules :</b> {{ props.item.validationRules }}<br>
|
|
204
|
+
</template>
|
|
205
|
+
<template v-if="props.item.inputOptions">
|
|
206
|
+
<b>Input Options :</b> {{ props.item.inputOptions }}<br>
|
|
207
|
+
</template>
|
|
208
|
+
<template v-if="props.item.inputAttributes">
|
|
209
|
+
<b>Input Attributes :</b> {{ props.item.inputAttributes }}<br>
|
|
210
|
+
</template>
|
|
211
|
+
</template>
|
|
212
|
+
</FormTable>
|
|
213
|
+
<v-textarea
|
|
214
|
+
v-else
|
|
215
|
+
v-model="templateAdvanceCode"
|
|
216
|
+
/>
|
|
217
|
+
</template>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, defineModel, computed, watch, watchEffect } from 'vue'
|
|
3
|
+
import { cloneDeep, isEqual } from 'lodash-es'
|
|
4
|
+
|
|
5
|
+
export interface FormDialogCallback {
|
|
6
|
+
done: Function
|
|
7
|
+
error: Function
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
maxWidth?: number | string
|
|
12
|
+
fullscreen?: boolean
|
|
13
|
+
title?: string
|
|
14
|
+
initialData?: object
|
|
15
|
+
formData?: object
|
|
16
|
+
saveCaption?: string
|
|
17
|
+
cancelCaption?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
saveCaption: 'บันทึก',
|
|
22
|
+
cancelCaption: 'ยกเลิก',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const isShowing = defineModel<boolean>({ default: false })
|
|
26
|
+
const isSaving = ref<boolean>(false)
|
|
27
|
+
const formPadRef = ref()
|
|
28
|
+
const formData = ref<object>({})
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits(['create', 'update'])
|
|
31
|
+
|
|
32
|
+
function save() {
|
|
33
|
+
if (formPadRef.value.isValid) {
|
|
34
|
+
isSaving.value = true
|
|
35
|
+
emit((isCreating.value) ? 'create' : 'update', formData.value, callback)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function cancel() {
|
|
40
|
+
isShowing.value = false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const callback: FormDialogCallback = {
|
|
44
|
+
done: function () {
|
|
45
|
+
isSaving.value = false
|
|
46
|
+
isShowing.value = false
|
|
47
|
+
},
|
|
48
|
+
error: function () {
|
|
49
|
+
isSaving.value = false
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isDataChange = computed(() => {
|
|
54
|
+
return !((isCreating.value) ? isEqual(formData.value, createOriginalValue.value) : isEqual(formData.value, props.formData))
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const isCreating = computed(() => {
|
|
58
|
+
return !props.formData
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const createOriginalValue = computed(() => {
|
|
62
|
+
return Object.assign({}, props.initialData)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
watchEffect(() => {
|
|
66
|
+
if (props.formData) {
|
|
67
|
+
formData.value = cloneDeep(props.formData)
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
formData.value = Object.assign({}, props.initialData)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
watch(() => isShowing.value, (newValue) => {
|
|
75
|
+
if (!newValue) formPadRef.value.reset()
|
|
76
|
+
})
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<v-dialog
|
|
81
|
+
v-model="isShowing"
|
|
82
|
+
:fullscreen="fullscreen"
|
|
83
|
+
:max-width="maxWidth"
|
|
84
|
+
persistent
|
|
85
|
+
scrollable
|
|
86
|
+
>
|
|
87
|
+
<VCard>
|
|
88
|
+
<VToolbar>
|
|
89
|
+
<VToolbarTitle>
|
|
90
|
+
<slot name="title">
|
|
91
|
+
{{ (isCreating) ? "New" : "Edit" }} {{ title }}
|
|
92
|
+
</slot>
|
|
93
|
+
</VToolbarTitle>
|
|
94
|
+
<VSpacer />
|
|
95
|
+
<VToolbarItems>
|
|
96
|
+
<VBtn
|
|
97
|
+
icon="mdi mdi-close"
|
|
98
|
+
@click="cancel"
|
|
99
|
+
/>
|
|
100
|
+
</VToolbarItems>
|
|
101
|
+
</VToolbar>
|
|
102
|
+
<VCardText>
|
|
103
|
+
<form-pad
|
|
104
|
+
ref="formPadRef"
|
|
105
|
+
v-model="formData"
|
|
106
|
+
>
|
|
107
|
+
<template #default="slotData">
|
|
108
|
+
<slot
|
|
109
|
+
v-bind="slotData"
|
|
110
|
+
:is-creating="isCreating"
|
|
111
|
+
:is-data-change="isDataChange"
|
|
112
|
+
/>
|
|
113
|
+
</template>
|
|
114
|
+
</form-pad>
|
|
115
|
+
</VCardText>
|
|
116
|
+
<VCardActions>
|
|
117
|
+
<VSpacer />
|
|
118
|
+
<VBtn
|
|
119
|
+
color="primary"
|
|
120
|
+
variant="flat"
|
|
121
|
+
:loading="isSaving"
|
|
122
|
+
:disabled="!isDataChange"
|
|
123
|
+
@click="save"
|
|
124
|
+
>
|
|
125
|
+
{{ saveCaption }}
|
|
126
|
+
</VBtn>
|
|
127
|
+
<VBtn
|
|
128
|
+
color="error"
|
|
129
|
+
variant="flat"
|
|
130
|
+
:disabled="isSaving"
|
|
131
|
+
@click="cancel"
|
|
132
|
+
>
|
|
133
|
+
{{ cancelCaption }}
|
|
134
|
+
</VBtn>
|
|
135
|
+
</VCardActions>
|
|
136
|
+
</VCard>
|
|
137
|
+
</v-dialog>
|
|
138
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { watch } from 'vue'
|
|
3
|
+
import { cloneDeep } from 'lodash-es'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
itemValue?: any
|
|
7
|
+
hook?: Function
|
|
8
|
+
modelValue?: any
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = defineProps<Props>()
|
|
12
|
+
const emit = defineEmits(['update:modelValue'])
|
|
13
|
+
|
|
14
|
+
watch(() => props.itemValue, (newValue) => {
|
|
15
|
+
const resultValue = cloneDeep(newValue)
|
|
16
|
+
if (props.hook) {
|
|
17
|
+
Promise.resolve(props.hook(resultValue, props.modelValue)).then((result) => {
|
|
18
|
+
emit('update:modelValue', result)
|
|
19
|
+
}).catch(e => void e)
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
emit('update:modelValue', resultValue)
|
|
23
|
+
}
|
|
24
|
+
}, { deep: true, immediate: true })
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<input
|
|
29
|
+
type="hidden"
|
|
30
|
+
v-bind="$attrs"
|
|
31
|
+
>
|
|
32
|
+
</template>
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-explicit-any,import/no-self-import */
|
|
3
|
-
import { compile, defineComponent, inject, onMounted, ref, shallowRef, watch, computed, withDefaults } from 'vue'
|
|
3
|
+
import { compile, defineComponent, inject, onMounted, ref, shallowRef, watch, computed, withDefaults, defineOptions } from 'vue'
|
|
4
4
|
import { isObject } from 'lodash-es'
|
|
5
5
|
import { watchDebounced } from '@vueuse/core'
|
|
6
6
|
import { useRules } from '../../composables/utils/validation'
|
|
7
|
+
import { useDocumentTemplate } from '../../composables/document/template'
|
|
7
8
|
import FormPad from './Pad.vue'
|
|
8
9
|
|
|
10
|
+
defineOptions({
|
|
11
|
+
inheritAttrs: false,
|
|
12
|
+
})
|
|
13
|
+
|
|
9
14
|
interface Props {
|
|
10
15
|
modelValue?: object
|
|
11
16
|
template?: any
|
|
@@ -34,13 +39,15 @@ watch(() => props.readonly, (newValue) => {
|
|
|
34
39
|
|
|
35
40
|
const { rules } = useRules()
|
|
36
41
|
|
|
37
|
-
const trimmedTemplate =
|
|
42
|
+
const trimmedTemplate = ref<string>('')
|
|
43
|
+
|
|
44
|
+
const vueFunctions = { ref, shallowRef, computed, watch, onMounted }
|
|
38
45
|
|
|
39
46
|
const templateScriptFunction = computed(() => {
|
|
40
47
|
let templateScript = props.templateScript?.trim() || 'return {}'
|
|
41
48
|
const pattern = /^\s*[{[].*[}\]]\s*$/
|
|
42
49
|
if (pattern.test(templateScript)) templateScript = 'return {}'
|
|
43
|
-
return Function('props', 'ctx', templateScript)
|
|
50
|
+
return Function('props', 'ctx', ...Object.keys(vueFunctions), templateScript)
|
|
44
51
|
})
|
|
45
52
|
|
|
46
53
|
const formPad = ref()
|
|
@@ -91,7 +98,7 @@ function buildFormComponent() {
|
|
|
91
98
|
validate: () => formPadTemplate.value.validate(),
|
|
92
99
|
resetValidate: () => formPadTemplate.value.resetValidate(),
|
|
93
100
|
isValid,
|
|
94
|
-
...templateScriptFunction.value(props, ctx),
|
|
101
|
+
...templateScriptFunction.value(props, ctx, ...Object.values(vueFunctions)),
|
|
95
102
|
}
|
|
96
103
|
},
|
|
97
104
|
template: componentTemplate,
|
|
@@ -129,8 +136,11 @@ onMounted(() => {
|
|
|
129
136
|
buildFormComponent()
|
|
130
137
|
})
|
|
131
138
|
|
|
132
|
-
watchDebounced(() => props.template,
|
|
133
|
-
|
|
139
|
+
watchDebounced(() => props.template, (newValue) => {
|
|
140
|
+
trimmedTemplate.value = useDocumentTemplate(newValue).trim() || ''
|
|
141
|
+
buildFormComponent()
|
|
142
|
+
}, { debounce: 500, maxWait: 5000, deep: true, immediate: true })
|
|
143
|
+
watchDebounced(() => props.templateScript, buildFormComponent, { debounce: 500, maxWait: 5000 })
|
|
134
144
|
|
|
135
145
|
defineExpose({
|
|
136
146
|
isValid,
|
|
@@ -148,6 +158,7 @@ defineExpose({
|
|
|
148
158
|
ref="formPad"
|
|
149
159
|
:disabled="disabled"
|
|
150
160
|
:readonly="readonly"
|
|
161
|
+
:class="$attrs.class"
|
|
151
162
|
>
|
|
152
163
|
<template #default="formProvided">
|
|
153
164
|
<slot
|
|
@@ -175,5 +186,6 @@ defineExpose({
|
|
|
175
186
|
v-model="formData"
|
|
176
187
|
:disabled="disabled"
|
|
177
188
|
:readonly="readonly"
|
|
189
|
+
:class="$attrs.class"
|
|
178
190
|
/>
|
|
179
191
|
</template>
|