vue-ready-modular 1.0.0 → 1.0.2
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/lib/generator.js +30 -7
- package/lib/templates/modal-add.template.js +28 -31
- package/lib/templates/modal-common-form.template.js +55 -0
- package/lib/templates/modal-delete.template.js +16 -6
- package/lib/templates/modal-edit.template.js +21 -32
- package/lib/templates/modal-view.template.js +47 -21
- package/lib/templates/mutation.template.js +34 -10
- package/lib/templates/page.template.js +99 -23
- package/lib/templates/query.template.js +33 -10
- package/lib/templates/routes.template.js +11 -7
- package/lib/templates/service.template.js +37 -10
- package/lib/templates/store.template.js +0 -1
- package/lib/utils/string.js +7 -3
- package/package.json +14 -8
package/lib/generator.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
|
+
import { plural, capitalize } from "./utils/string.js";
|
|
3
4
|
|
|
4
5
|
import { createDir, createFile } from "./utils/file.js";
|
|
5
|
-
import { capitalize } from "./utils/string.js";
|
|
6
6
|
|
|
7
7
|
// Templates
|
|
8
8
|
import indexTemplate from "./templates/index.template.js";
|
|
@@ -14,6 +14,7 @@ import queryTemplate from "./templates/query.template.js";
|
|
|
14
14
|
import mutationTemplate from "./templates/mutation.template.js";
|
|
15
15
|
import pageTemplate from "./templates/page.template.js";
|
|
16
16
|
import addModalTemplate from "./templates/modal-add.template.js";
|
|
17
|
+
import commonFormTemplate from "./templates/modal-common-form.template.js";
|
|
17
18
|
import editModalTemplate from "./templates/modal-edit.template.js";
|
|
18
19
|
import viewModalTemplate from "./templates/modal-view.template.js";
|
|
19
20
|
import deleteModalTemplate from "./templates/modal-delete.template.js";
|
|
@@ -60,17 +61,39 @@ export function createModule(name) {
|
|
|
60
61
|
createFile(`${basePath}/stores/${module}Store.js`, storeTemplate(ctx));
|
|
61
62
|
createFile(`${basePath}/data/${module}Data.js`, dataTemplate(ctx));
|
|
62
63
|
createFile(`${basePath}/services/${module}Service.js`, serviceTemplate(ctx));
|
|
63
|
-
createFile(
|
|
64
|
-
|
|
64
|
+
createFile(
|
|
65
|
+
`${basePath}/queries/use${capitalize(plural(Module))}Query.js`,
|
|
66
|
+
queryTemplate(ctx)
|
|
67
|
+
);
|
|
68
|
+
createFile(
|
|
69
|
+
`${basePath}/queries/use${Module}Mutations.js`,
|
|
70
|
+
mutationTemplate(ctx)
|
|
71
|
+
);
|
|
65
72
|
|
|
66
73
|
// -----------------------------
|
|
67
74
|
// 4. Create page & modal components
|
|
68
75
|
// -----------------------------
|
|
69
76
|
createFile(`${basePath}/pages/${Module}Page.vue`, pageTemplate(ctx));
|
|
70
|
-
createFile(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
createFile(
|
|
78
|
+
`${basePath}/pages/components/AddModal.vue`,
|
|
79
|
+
addModalTemplate(ctx)
|
|
80
|
+
);
|
|
81
|
+
createFile(
|
|
82
|
+
`${basePath}/pages/components/CommonForm.vue`,
|
|
83
|
+
commonFormTemplate(ctx)
|
|
84
|
+
);
|
|
85
|
+
createFile(
|
|
86
|
+
`${basePath}/pages/components/EditModal.vue`,
|
|
87
|
+
editModalTemplate(ctx)
|
|
88
|
+
);
|
|
89
|
+
createFile(
|
|
90
|
+
`${basePath}/pages/components/ViewModal.vue`,
|
|
91
|
+
viewModalTemplate(ctx)
|
|
92
|
+
);
|
|
93
|
+
createFile(
|
|
94
|
+
`${basePath}/pages/components/DeleteModal.vue`,
|
|
95
|
+
deleteModalTemplate(ctx)
|
|
96
|
+
);
|
|
74
97
|
|
|
75
98
|
console.log(`✅ Module "${module}" created successfully`);
|
|
76
99
|
}
|
|
@@ -1,57 +1,54 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/templates/add-modal.template.js
|
|
2
|
+
export default ({ name, Name }) => `
|
|
3
|
+
<template>
|
|
2
4
|
<BaseModal
|
|
3
5
|
:isVisible="store.isModal"
|
|
4
6
|
:title="\`Add \${store.moduleName}\`"
|
|
5
7
|
:maxWidth="\`\${store.modalWidth}\`"
|
|
6
8
|
@close="store.handleToggleModal"
|
|
7
9
|
>
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
id="name"
|
|
14
|
-
v-model="formData.name"
|
|
15
|
-
:required="true"
|
|
16
|
-
placeholder="Eg: Example Name"
|
|
17
|
-
/>
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
<!-- Actions -->
|
|
21
|
-
<div class="flex justify-end gap-2 pt-4">
|
|
22
|
-
<BaseButton
|
|
23
|
-
class="bg-yellow-600 hover:bg-yellow-700"
|
|
24
|
-
type="button"
|
|
25
|
-
@click="store.handleToggleModal"
|
|
26
|
-
>Cancel</BaseButton>
|
|
27
|
-
<BaseButton type="submit">Save</BaseButton>
|
|
28
|
-
</div>
|
|
29
|
-
</form>
|
|
10
|
+
<CommonForm
|
|
11
|
+
v-model:formData="formData"
|
|
12
|
+
:onSubmit="handleSubmit"
|
|
13
|
+
:onCancel="store.handleToggleModal"
|
|
14
|
+
/>
|
|
30
15
|
</BaseModal>
|
|
31
16
|
</template>
|
|
32
17
|
|
|
33
18
|
<script setup>
|
|
34
19
|
import { ref } from 'vue'
|
|
20
|
+
import CommonForm from './CommonForm.vue'
|
|
35
21
|
import { use${Name}Mutations } from '@/modules/${name}/queries/use${Name}Mutations'
|
|
36
22
|
import app from '@/shared/config/appConfig'
|
|
37
|
-
import { use${Name}Store } from '@/modules/${name}/
|
|
23
|
+
import { use${Name}Store } from '@/modules/${name}/store/${name}Store'
|
|
38
24
|
|
|
39
25
|
const store = use${Name}Store()
|
|
40
26
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
// Default Form Data
|
|
28
|
+
const defaultFormData = {
|
|
29
|
+
name: 'Test Name',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Initialize formData
|
|
33
|
+
const formData = ref(
|
|
34
|
+
app.moduleLocal
|
|
35
|
+
? { ...defaultFormData }
|
|
36
|
+
: Object.fromEntries(
|
|
37
|
+
Object.keys(defaultFormData).map((key) => [key, ''])
|
|
38
|
+
)
|
|
39
|
+
)
|
|
44
40
|
|
|
45
41
|
const { submit } = use${Name}Mutations(store.moduleName, {
|
|
46
42
|
onSuccess() {
|
|
47
|
-
store.handleToggleModal()
|
|
48
|
-
store.handleReset(formData.value)
|
|
43
|
+
store.handleToggleModal()
|
|
44
|
+
store.handleReset(formData.value)
|
|
49
45
|
},
|
|
50
|
-
onError
|
|
51
|
-
console.log('
|
|
46
|
+
onError(error) {
|
|
47
|
+
console.log('Error:', error)
|
|
52
48
|
},
|
|
53
49
|
})
|
|
54
50
|
|
|
51
|
+
// Submit handler
|
|
55
52
|
const handleSubmit = async () => {
|
|
56
53
|
await submit.mutateAsync(formData.value)
|
|
57
54
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// lib/templates/common-form.template.js
|
|
2
|
+
export default ({ Name }) => `
|
|
3
|
+
<template>
|
|
4
|
+
<form @submit.prevent="onSubmit" class="space-y-4">
|
|
5
|
+
<!-- ${Name} Name -->
|
|
6
|
+
<div class="space-y-2">
|
|
7
|
+
<BaseLabel for="name">Name</BaseLabel>
|
|
8
|
+
<BaseInput
|
|
9
|
+
id="name"
|
|
10
|
+
v-model="localForm.name"
|
|
11
|
+
placeholder="Eg: ${Name} Name"
|
|
12
|
+
:required="true"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- Actions -->
|
|
17
|
+
<div class="flex justify-end gap-2 pt-4">
|
|
18
|
+
<BaseButton
|
|
19
|
+
class="bg-yellow-600 hover:bg-yellow-700"
|
|
20
|
+
type="button"
|
|
21
|
+
@click="onCancel"
|
|
22
|
+
>
|
|
23
|
+
Cancel
|
|
24
|
+
</BaseButton>
|
|
25
|
+
<BaseButton type="submit">Save</BaseButton>
|
|
26
|
+
</div>
|
|
27
|
+
</form>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup>
|
|
31
|
+
import { ref, watch } from 'vue'
|
|
32
|
+
|
|
33
|
+
// Props
|
|
34
|
+
const props = defineProps({
|
|
35
|
+
formData: { type: Object, required: true },
|
|
36
|
+
onSubmit: { type: Function, required: true },
|
|
37
|
+
onCancel: { type: Function, required: true },
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Emit event to sync formData
|
|
41
|
+
const emit = defineEmits(['update:formData'])
|
|
42
|
+
|
|
43
|
+
// Local reactive copy
|
|
44
|
+
const localForm = ref({ ...props.formData })
|
|
45
|
+
|
|
46
|
+
// Sync to parent when local changes
|
|
47
|
+
watch(
|
|
48
|
+
localForm,
|
|
49
|
+
(newVal) => {
|
|
50
|
+
emit('update:formData', newVal)
|
|
51
|
+
},
|
|
52
|
+
{ deep: true }
|
|
53
|
+
)
|
|
54
|
+
</script>
|
|
55
|
+
`;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/templates/delete-modal.template.js
|
|
2
|
+
export default ({ name, Name }) => `
|
|
3
|
+
<template>
|
|
2
4
|
<BaseModal
|
|
3
5
|
:isVisible="store.isDeleteModal"
|
|
4
6
|
:title="\`Are you sure you want to delete this \${store.moduleName}?\`"
|
|
@@ -23,8 +25,16 @@ export default ({ name, Name }) => `<template>
|
|
|
23
25
|
class="bg-yellow-600 hover:bg-yellow-700"
|
|
24
26
|
type="button"
|
|
25
27
|
@click="store.handleToggleModal"
|
|
26
|
-
>
|
|
27
|
-
|
|
28
|
+
>
|
|
29
|
+
Cancel
|
|
30
|
+
</BaseButton>
|
|
31
|
+
|
|
32
|
+
<BaseButton
|
|
33
|
+
type="submit"
|
|
34
|
+
class="bg-red-600 hover:bg-red-700"
|
|
35
|
+
>
|
|
36
|
+
Delete
|
|
37
|
+
</BaseButton>
|
|
28
38
|
</div>
|
|
29
39
|
</form>
|
|
30
40
|
</BaseModal>
|
|
@@ -32,15 +42,15 @@ export default ({ name, Name }) => `<template>
|
|
|
32
42
|
|
|
33
43
|
<script setup>
|
|
34
44
|
import { use${Name}Mutations } from '@/modules/${name}/queries/use${Name}Mutations'
|
|
35
|
-
import { use${Name}Store } from '@/modules/${name}/
|
|
45
|
+
import { use${Name}Store } from '@/modules/${name}/store/${name}Store'
|
|
36
46
|
|
|
37
47
|
const store = use${Name}Store()
|
|
38
48
|
|
|
39
49
|
const { remove } = use${Name}Mutations(store.moduleName, {
|
|
40
50
|
onSuccess() {
|
|
41
|
-
store.handleToggleModal() //
|
|
51
|
+
store.handleToggleModal() // close modal
|
|
42
52
|
},
|
|
43
|
-
onError
|
|
53
|
+
onError(error) {
|
|
44
54
|
console.log('Custom error handling', error)
|
|
45
55
|
},
|
|
46
56
|
})
|
|
@@ -1,67 +1,56 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/templates/edit-modal.template.js
|
|
2
|
+
export default ({ name, Name }) => `
|
|
3
|
+
<template>
|
|
2
4
|
<BaseModal
|
|
3
5
|
:isVisible="store.isEditModal"
|
|
4
6
|
:title="\`Edit \${store.moduleName}\`"
|
|
5
7
|
:maxWidth="\`\${store.modalWidth}\`"
|
|
6
8
|
@close="store.handleToggleModal"
|
|
7
9
|
>
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
id="name"
|
|
14
|
-
v-model="formData.name"
|
|
15
|
-
placeholder="Eg: Example Name"
|
|
16
|
-
/>
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
<!-- Actions -->
|
|
20
|
-
<div class="flex justify-end gap-2 pt-4">
|
|
21
|
-
<BaseButton
|
|
22
|
-
class="bg-yellow-600 hover:bg-yellow-700"
|
|
23
|
-
type="button"
|
|
24
|
-
@click="store.handleToggleModal"
|
|
25
|
-
>Cancel</BaseButton>
|
|
26
|
-
<BaseButton type="submit">Save</BaseButton>
|
|
27
|
-
</div>
|
|
28
|
-
</form>
|
|
10
|
+
<CommonForm
|
|
11
|
+
v-model:formData="formData"
|
|
12
|
+
:onSubmit="handleSubmit"
|
|
13
|
+
:onCancel="store.handleToggleModal"
|
|
14
|
+
/>
|
|
29
15
|
</BaseModal>
|
|
30
16
|
</template>
|
|
31
17
|
|
|
32
18
|
<script setup>
|
|
33
19
|
import { ref, watch } from 'vue'
|
|
20
|
+
import CommonForm from './CommonForm.vue'
|
|
34
21
|
import { use${Name}Mutations } from '@/modules/${name}/queries/use${Name}Mutations'
|
|
35
|
-
import { use${Name}Store } from '@/modules/${name}/
|
|
22
|
+
import { use${Name}Store } from '@/modules/${name}/store/${name}Store'
|
|
36
23
|
|
|
37
24
|
const store = use${Name}Store()
|
|
38
25
|
|
|
26
|
+
// Form state
|
|
39
27
|
const formData = ref({
|
|
40
|
-
name: '',
|
|
41
28
|
id: '',
|
|
29
|
+
name: '',
|
|
42
30
|
})
|
|
43
31
|
|
|
44
|
-
//
|
|
32
|
+
// Populate form when store.item updates
|
|
45
33
|
watch(
|
|
46
34
|
() => store.item,
|
|
47
|
-
(
|
|
48
|
-
if (
|
|
49
|
-
formData.value
|
|
50
|
-
formData.value.id = newItem.id || ''
|
|
35
|
+
(item) => {
|
|
36
|
+
if (item) {
|
|
37
|
+
Object.assign(formData.value, item)
|
|
51
38
|
}
|
|
52
39
|
},
|
|
53
40
|
{ immediate: true }
|
|
54
41
|
)
|
|
55
42
|
|
|
43
|
+
// Mutation handler
|
|
56
44
|
const { update } = use${Name}Mutations(store.moduleName, {
|
|
57
45
|
onSuccess() {
|
|
58
|
-
store.handleToggleModal() //
|
|
46
|
+
store.handleToggleModal() // close modal
|
|
59
47
|
},
|
|
60
|
-
onError
|
|
61
|
-
console.log('Custom error handling', error)
|
|
48
|
+
onError(error) {
|
|
49
|
+
console.log('Custom error handling:', error)
|
|
62
50
|
},
|
|
63
51
|
})
|
|
64
52
|
|
|
53
|
+
// Submit
|
|
65
54
|
const handleSubmit = async () => {
|
|
66
55
|
await update.mutateAsync(formData.value)
|
|
67
56
|
}
|
|
@@ -1,33 +1,59 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/templates/view-modal.template.js
|
|
2
|
+
export default ({ name, Name }) => `
|
|
3
|
+
<template>
|
|
2
4
|
<BaseModal
|
|
3
5
|
:isVisible="store.isViewModal"
|
|
4
6
|
:title="\`View \${store.moduleName} Details\`"
|
|
5
|
-
:maxWidth="\`\${store.modalWidth}\`"
|
|
7
|
+
:maxWidth="\`\${store.modalWidth || '60vw'}\`"
|
|
6
8
|
@close="store.handleToggleModal"
|
|
7
9
|
>
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
<ViewModalLayout>
|
|
11
|
+
<div class="space-y-2">
|
|
12
|
+
<p class="text-lg">
|
|
13
|
+
<strong>ID:</strong>
|
|
14
|
+
{{ store.item?.id }}
|
|
15
|
+
</p>
|
|
16
|
+
<p class="text-lg">
|
|
17
|
+
<strong>Name:</strong>
|
|
18
|
+
{{ store.item?.name }}
|
|
19
|
+
</p>
|
|
20
|
+
<p class="text-lg">
|
|
21
|
+
<strong>Created At:</strong>
|
|
22
|
+
{{ store.item?.created_at }}
|
|
23
|
+
</p>
|
|
24
|
+
<p class="text-lg">
|
|
25
|
+
<strong>Updated At:</strong>
|
|
26
|
+
{{ store.item?.updated_at }}
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- Related Items -->
|
|
31
|
+
<h2 class="text-lg font-semibold py-3">
|
|
32
|
+
{{ store.item?.name }} Clients
|
|
33
|
+
</h2>
|
|
34
|
+
|
|
35
|
+
<BaseTable
|
|
36
|
+
v-if="store.item?.clients"
|
|
37
|
+
:columns="columns"
|
|
38
|
+
:rows="store.item.clients"
|
|
39
|
+
/>
|
|
40
|
+
</ViewModalLayout>
|
|
26
41
|
</BaseModal>
|
|
27
42
|
</template>
|
|
28
43
|
|
|
29
44
|
<script setup>
|
|
30
|
-
import { use${Name}Store } from '@/modules/${name}/
|
|
45
|
+
import { use${Name}Store } from '@/modules/${name}/store/${name}Store'
|
|
46
|
+
import ViewModalLayout from '@/shared/components/ui/ViewModalLayout.vue'
|
|
47
|
+
import { useCrudTable } from '@/shared/composables/useCrudTable'
|
|
48
|
+
|
|
31
49
|
const store = use${Name}Store()
|
|
50
|
+
|
|
51
|
+
const { columns } = useCrudTable(store, [
|
|
52
|
+
{ key: 'name', label: 'Client' },
|
|
53
|
+
{ key: 'email', label: 'Email' },
|
|
54
|
+
{ key: 'phone', label: 'Phone' },
|
|
55
|
+
{ key: 'client_id', label: 'Client ID' },
|
|
56
|
+
{ key: 'country', label: '${Name}' },
|
|
57
|
+
])
|
|
32
58
|
</script>
|
|
33
59
|
`;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// lib/templates/use-mutations.template.js
|
|
2
|
+
export default ({ name, Name }) => `
|
|
3
|
+
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
|
4
|
+
import {
|
|
5
|
+
submitData,
|
|
6
|
+
updateData,
|
|
7
|
+
deleteItem,
|
|
8
|
+
bulkDelete
|
|
9
|
+
} from '../services/${name}Service'
|
|
6
10
|
import { toast } from '@/shared/config/toastConfig'
|
|
7
11
|
|
|
8
12
|
export function use${Name}Mutations(moduleName, options = {}) {
|
|
@@ -11,7 +15,7 @@ export function use${Name}Mutations(moduleName, options = {}) {
|
|
|
11
15
|
const handleSuccess = (data, variables) => {
|
|
12
16
|
toast.success(\`\${moduleName} operation successful\`)
|
|
13
17
|
queryClient.invalidateQueries(['${name}s'])
|
|
14
|
-
options.onSuccess?.(data, variables)
|
|
18
|
+
options.onSuccess?.(data, variables)
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
const handleError = (error) => {
|
|
@@ -20,10 +24,30 @@ export function use${Name}Mutations(moduleName, options = {}) {
|
|
|
20
24
|
options.onError?.(error)
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
const submit = useMutation({
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
const submit = useMutation({
|
|
28
|
+
mutationFn: submitData,
|
|
29
|
+
onSuccess: handleSuccess,
|
|
30
|
+
onError: handleError,
|
|
31
|
+
})
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
const update = useMutation({
|
|
34
|
+
mutationFn: updateData,
|
|
35
|
+
onSuccess: handleSuccess,
|
|
36
|
+
onError: handleError,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const remove = useMutation({
|
|
40
|
+
mutationFn: deleteItem,
|
|
41
|
+
onSuccess: handleSuccess,
|
|
42
|
+
onError: handleError,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const removeItems = useMutation({
|
|
46
|
+
mutationFn: bulkDelete,
|
|
47
|
+
onSuccess: handleSuccess,
|
|
48
|
+
onError: handleError,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return { submit, update, remove, removeItems }
|
|
28
52
|
}
|
|
29
53
|
`;
|
|
@@ -1,21 +1,78 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/templates/page.template.js
|
|
2
|
+
import { plural, capitalize } from "../utils/string.js";
|
|
3
|
+
|
|
4
|
+
export default ({ name, Name }) => {
|
|
5
|
+
const pluralName = capitalize(plural(Name)); // Country -> Countries
|
|
6
|
+
|
|
7
|
+
return `<template>
|
|
2
8
|
<div class="min-h-screen bg-gray-50">
|
|
9
|
+
<!-- Page Header -->
|
|
3
10
|
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
4
|
-
<
|
|
11
|
+
<div>
|
|
12
|
+
<PageTitle>{{ store.moduleName }} List</PageTitle>
|
|
13
|
+
</div>
|
|
5
14
|
<BaseButton @click="store.handleToggleModal('add')">
|
|
6
15
|
Add {{ store.moduleName }}
|
|
7
16
|
</BaseButton>
|
|
8
17
|
</div>
|
|
9
18
|
|
|
19
|
+
<!-- Bulk Delete & Filters -->
|
|
20
|
+
<div class="flex justify-between items-center my-4">
|
|
21
|
+
<!-- BULK DELETE -->
|
|
22
|
+
<div>
|
|
23
|
+
<BaseButton
|
|
24
|
+
v-if="selectedIds.length"
|
|
25
|
+
class="bg-red-600 text-white hover:bg-red-700"
|
|
26
|
+
@click="bulkDelete"
|
|
27
|
+
>
|
|
28
|
+
Delete Selected ({{ selectedIds.length }})
|
|
29
|
+
</BaseButton>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- FILTERS -->
|
|
33
|
+
<TableFilters
|
|
34
|
+
:filters="filters"
|
|
35
|
+
:has-active-filters="hasActiveFilters"
|
|
36
|
+
@reset="resetFilters"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- Content Card -->
|
|
10
41
|
<div class="rounded-lg bg-white shadow-sm">
|
|
11
42
|
<BaseTable
|
|
12
43
|
v-if="!isLoading"
|
|
13
44
|
:columns="columns"
|
|
14
45
|
:rows="rows"
|
|
15
46
|
show-actions
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@
|
|
47
|
+
selectable
|
|
48
|
+
:selected-ids="selectedIds"
|
|
49
|
+
@toggleAll="(checked) => toggleAll(rows, checked)"
|
|
50
|
+
@toggleRow="toggleRow"
|
|
51
|
+
>
|
|
52
|
+
<template #actions="{ row }">
|
|
53
|
+
<button @click="onView(row)" class="text-blue-600 cursor-pointer">
|
|
54
|
+
<i class="fa fa-eye"></i>
|
|
55
|
+
</button>
|
|
56
|
+
|
|
57
|
+
<button @click="onEdit(row)" class="text-green-600 cursor-pointer">
|
|
58
|
+
<i class="fa fa-pencil"></i>
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
<button @click="onDelete(row)" class="text-red-600 cursor-pointer">
|
|
62
|
+
<i class="fa fa-trash"></i>
|
|
63
|
+
</button>
|
|
64
|
+
</template>
|
|
65
|
+
</BaseTable>
|
|
66
|
+
|
|
67
|
+
<!-- Pagination -->
|
|
68
|
+
<BasePagination
|
|
69
|
+
v-if="!isLoading"
|
|
70
|
+
:total="total"
|
|
71
|
+
:showing="showing"
|
|
72
|
+
:links="links"
|
|
73
|
+
:per-page="perPage"
|
|
74
|
+
@update:page="setPage"
|
|
75
|
+
@update:perPage="setPerPage"
|
|
19
76
|
/>
|
|
20
77
|
|
|
21
78
|
<AddModal />
|
|
@@ -28,8 +85,16 @@ export default ({ name, Name }) => `<template>
|
|
|
28
85
|
|
|
29
86
|
<script setup>
|
|
30
87
|
import { computed, defineAsyncComponent } from 'vue'
|
|
31
|
-
import { use${
|
|
32
|
-
import { use${Name}Store } from '../
|
|
88
|
+
import { use${pluralName}Query } from '../queries/use${pluralName}Query'
|
|
89
|
+
import { use${Name}Store } from '../store/${name}Store'
|
|
90
|
+
import { use${Name}Mutations } from '../queries/use${Name}Mutations'
|
|
91
|
+
|
|
92
|
+
import { usePagination } from '@/shared/composables/usePagination'
|
|
93
|
+
import { useBulkDelete } from '@/shared/composables/useBulkDelete'
|
|
94
|
+
import { useCrudTable } from '@/shared/composables/useCrudTable'
|
|
95
|
+
import { useTableFilters } from '@/shared/composables/useTableFilters'
|
|
96
|
+
|
|
97
|
+
import TableFilters from '@/shared/components/ui/TableFilters.vue'
|
|
33
98
|
|
|
34
99
|
const ViewModal = defineAsyncComponent(() => import('./components/ViewModal.vue'))
|
|
35
100
|
const AddModal = defineAsyncComponent(() => import('./components/AddModal.vue'))
|
|
@@ -37,25 +102,36 @@ const EditModal = defineAsyncComponent(() => import('./components/EditModal.vue'
|
|
|
37
102
|
const DeleteModal = defineAsyncComponent(() => import('./components/DeleteModal.vue'))
|
|
38
103
|
|
|
39
104
|
const store = use${Name}Store()
|
|
40
|
-
const { data, isLoading } = use${Name}sQuery()
|
|
41
105
|
|
|
42
|
-
|
|
43
|
-
|
|
106
|
+
/* ---------------- Filters ---------------- */
|
|
107
|
+
const { filters, hasActiveFilters, resetFilters } = useTableFilters({
|
|
108
|
+
searchQuery: '',
|
|
109
|
+
from_date: null,
|
|
110
|
+
to_date: null,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/* ---------------- Pagination ---------------- */
|
|
114
|
+
const pagination = usePagination()
|
|
115
|
+
const { page, perPage, total, showing, links, setPage, setPerPage } = pagination
|
|
116
|
+
|
|
117
|
+
/* ---------------- Query ---------------- */
|
|
118
|
+
const { data, isLoading } = use${pluralName}Query(page, perPage, filters)
|
|
119
|
+
pagination.bindMeta(data)
|
|
120
|
+
|
|
121
|
+
/* ---------------- Mutations ---------------- */
|
|
122
|
+
const { removeItems } = use${Name}Mutations(store.moduleName)
|
|
123
|
+
|
|
124
|
+
/* ---------------- Bulk Delete ---------------- */
|
|
125
|
+
const { selectedIds, toggleAll, toggleRow, bulkDelete } = useBulkDelete(removeItems, {
|
|
126
|
+
confirmText: 'Are you sure to delete selected records?',
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
/* ---------------- Table ---------------- */
|
|
130
|
+
const { columns, onView, onEdit, onDelete } = useCrudTable(store, [
|
|
44
131
|
{ key: 'name', label: '${Name}' },
|
|
45
|
-
|
|
46
|
-
{ key: 'updated_at', label: 'Updated At' }
|
|
47
|
-
]
|
|
132
|
+
])
|
|
48
133
|
|
|
49
134
|
const rows = computed(() => data.value?.data?.data ?? [])
|
|
50
|
-
|
|
51
|
-
function onView(row) {
|
|
52
|
-
store.handleToggleModal('view', row)
|
|
53
|
-
}
|
|
54
|
-
function onEdit(row) {
|
|
55
|
-
store.handleToggleModal('edit', row)
|
|
56
|
-
}
|
|
57
|
-
function onDelete(row) {
|
|
58
|
-
store.handleToggleModal('delete', row)
|
|
59
|
-
}
|
|
60
135
|
</script>
|
|
61
136
|
`;
|
|
137
|
+
};
|
|
@@ -1,19 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { plural, capitalize } from "../utils/string.js";
|
|
2
|
+
|
|
3
|
+
export default ({ name, Name }) => {
|
|
4
|
+
const pluralName = plural(name); // country → countries, city → cities
|
|
5
|
+
|
|
6
|
+
return `import { useQuery } from '@tanstack/vue-query'
|
|
5
7
|
import { fetchAll } from '../services/${name}Service'
|
|
8
|
+
import { computed, unref } from 'vue'
|
|
9
|
+
|
|
10
|
+
export function use${capitalize(pluralName)}Query(pageRef, perPageRef, filtersRef) {
|
|
11
|
+
const page = computed(() => unref(pageRef))
|
|
12
|
+
const perPage = computed(() => unref(perPageRef))
|
|
13
|
+
const search = computed(() => unref(filtersRef)?.searchQuery?.trim())
|
|
14
|
+
const fromDate = computed(() => unref(filtersRef)?.from_date)
|
|
15
|
+
const toDate = computed(() => unref(filtersRef)?.to_date)
|
|
16
|
+
const cacheKey = '${pluralName}'
|
|
6
17
|
|
|
7
|
-
export function use${Name}sQuery(params = {}) {
|
|
8
18
|
return useQuery({
|
|
9
|
-
queryKey: [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
queryKey: computed(() => [
|
|
20
|
+
cacheKey,
|
|
21
|
+
page.value,
|
|
22
|
+
perPage.value,
|
|
23
|
+
search.value && search.value.length >= 3 ? search.value : 'all',
|
|
24
|
+
fromDate.value || 'all',
|
|
25
|
+
toDate.value || 'all',
|
|
26
|
+
]),
|
|
27
|
+
queryFn: () => fetchAll(page.value, perPage.value, {
|
|
28
|
+
search: search.value,
|
|
29
|
+
from_date: fromDate.value,
|
|
30
|
+
to_date: toDate.value,
|
|
31
|
+
}),
|
|
32
|
+
enabled: computed(() => !search.value || search.value.length >= 3),
|
|
33
|
+
staleTime: 5 * 60 * 1000,
|
|
34
|
+
cacheTime: 60 * 60 * 1000,
|
|
13
35
|
keepPreviousData: true,
|
|
14
36
|
meta: {
|
|
15
|
-
persist: true,
|
|
37
|
+
persist: true,
|
|
16
38
|
},
|
|
17
39
|
})
|
|
18
40
|
}
|
|
19
41
|
`;
|
|
42
|
+
};
|
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { plural } from "../utils/string.js";
|
|
2
|
+
|
|
3
|
+
export default ({ name, Name }) => {
|
|
4
|
+
const pathName = plural(name); // e.g., 'country' → 'countries'
|
|
5
|
+
const routeName = `${Name}List`; // e.g., 'CountryList'
|
|
6
|
+
|
|
7
|
+
return `import ${Name}Page from './pages/${Name}Page.vue'
|
|
5
8
|
import DashboardLayout from '@/shared/layouts/DashboardLayout.vue'
|
|
6
9
|
|
|
7
10
|
export default [
|
|
8
11
|
{
|
|
9
|
-
path: '/${
|
|
12
|
+
path: '/${pathName}',
|
|
10
13
|
component: DashboardLayout,
|
|
11
|
-
meta: { requiresAuth: true },
|
|
14
|
+
meta: { requiresAuth: true, roles: ['super_admin','admin','recruiter'] },
|
|
12
15
|
children: [
|
|
13
16
|
{
|
|
14
17
|
path: '',
|
|
15
|
-
name: '${
|
|
18
|
+
name: '${routeName}',
|
|
16
19
|
component: ${Name}Page,
|
|
17
20
|
},
|
|
18
21
|
],
|
|
19
22
|
},
|
|
20
23
|
]
|
|
21
24
|
`;
|
|
25
|
+
};
|
|
@@ -1,30 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
name,
|
|
3
|
-
Name,
|
|
4
|
-
}) => `import { useApi } from '@/shared/composables/useApi'
|
|
1
|
+
import { plural } from "../utils/string.js";
|
|
5
2
|
|
|
6
|
-
|
|
3
|
+
export default ({ name, Name }) => {
|
|
4
|
+
const baseUrl = `/${plural(name)}`;
|
|
5
|
+
|
|
6
|
+
return `import { useApi } from '@/shared/composables/useApi'
|
|
7
|
+
import { buildUrl } from '@/shared/utils/buildUrl'
|
|
8
|
+
|
|
9
|
+
const BASE_URL = '${baseUrl}'
|
|
7
10
|
|
|
8
11
|
const response = (api) => ({
|
|
9
12
|
data: api.data.value,
|
|
10
13
|
error: api.error.value,
|
|
11
14
|
})
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
// ✅ Fetch all with pagination & filters
|
|
17
|
+
export async function fetchAll(page = 1, perPage = 10, filters = {}) {
|
|
18
|
+
const api = useApi()
|
|
19
|
+
const url = buildUrl(BASE_URL, {
|
|
20
|
+
page,
|
|
21
|
+
per_page: perPage,
|
|
22
|
+
...filters,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
await api.sendRequest(url)
|
|
26
|
+
return response(api)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ✅ Fetch single item by ID
|
|
30
|
+
export const fetchOne = async (id) => {
|
|
14
31
|
const api = useApi()
|
|
15
|
-
await api.sendRequest(BASE_URL)
|
|
32
|
+
await api.sendRequest(\`\${BASE_URL}/\${id}\`)
|
|
16
33
|
return response(api)
|
|
17
34
|
}
|
|
18
35
|
|
|
36
|
+
// ✅ Submit new item
|
|
19
37
|
export async function submitData(payload) {
|
|
20
38
|
const api = useApi()
|
|
21
39
|
await api.sendRequest(BASE_URL, 'POST', payload)
|
|
22
|
-
if (api.error.value)
|
|
23
|
-
throw api.error.value // 🚨 so useMutation.onError triggers
|
|
24
|
-
}
|
|
40
|
+
if (api.error.value) throw api.error.value
|
|
25
41
|
return api.data.value
|
|
26
42
|
}
|
|
27
43
|
|
|
44
|
+
// ✅ Update existing item
|
|
28
45
|
export async function updateData(payload) {
|
|
29
46
|
const api = useApi()
|
|
30
47
|
await api.sendRequest(\`\${BASE_URL}/\${payload.id}\`, 'POST', payload, {
|
|
@@ -34,10 +51,20 @@ export async function updateData(payload) {
|
|
|
34
51
|
return api.data.value
|
|
35
52
|
}
|
|
36
53
|
|
|
54
|
+
// ✅ Delete single item
|
|
37
55
|
export async function deleteItem(id) {
|
|
38
56
|
const api = useApi()
|
|
39
57
|
await api.sendRequest(\`\${BASE_URL}/\${id}\`, 'DELETE')
|
|
40
58
|
if (api.error.value) throw api.error.value
|
|
41
59
|
return api.data.value
|
|
42
60
|
}
|
|
61
|
+
|
|
62
|
+
// ✅ Bulk delete multiple items
|
|
63
|
+
export async function bulkDelete(ids) {
|
|
64
|
+
const api = useApi()
|
|
65
|
+
await api.sendRequest(\`\${BASE_URL}/bulk-delete\`, 'POST', { ids })
|
|
66
|
+
if (api.error.value) throw api.error.value
|
|
67
|
+
return api.data.value
|
|
68
|
+
}
|
|
43
69
|
`;
|
|
70
|
+
};
|
package/lib/utils/string.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// lib/utils/string.js
|
|
2
|
+
import pluralize from 'pluralize'
|
|
3
|
+
|
|
4
|
+
export const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1)
|
|
5
|
+
|
|
6
|
+
// pluralize dynamically
|
|
7
|
+
export const plural = (str) => pluralize(str.toLowerCase())
|
package/package.json
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "vue-ready-modular",
|
|
3
|
-
"version": "1.0.
|
|
2
|
+
"name": "vue-ready-modular",
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Vue.js module generator",
|
|
5
|
-
"main": "lib/generator.js",
|
|
5
|
+
"main": "lib/generator.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"vue-modular": "./bin/cli.js"
|
|
7
|
+
"vue-modular": "./bin/cli.js"
|
|
8
8
|
},
|
|
9
|
-
"type": "module",
|
|
9
|
+
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "echo \"No tests yet\""
|
|
12
12
|
},
|
|
13
|
-
"keywords": [
|
|
13
|
+
"keywords": [
|
|
14
|
+
"vue",
|
|
15
|
+
"generator",
|
|
16
|
+
"module",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
14
19
|
"author": "Your Name",
|
|
15
20
|
"license": "MIT",
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"path": "^0.12.7"
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"path": "^0.12.7",
|
|
23
|
+
"pluralize": "^8.0.0"
|
|
18
24
|
}
|
|
19
25
|
}
|