@drax/crud-vue 0.4.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/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@drax/crud-vue",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "0.4.0",
7
+ "type": "module",
8
+ "main": "./src/index.ts",
9
+ "module": "./src/index.ts",
10
+ "types": "./src/index.ts",
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "scripts": {
15
+ "dev": "vite",
16
+ "build": "run-p type-check \"build-only {@}\" --",
17
+ "preview": "vite preview",
18
+ "test:unit": "vitest",
19
+ "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
20
+ "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
21
+ "build-only": "vite build",
22
+ "type-check": "vue-tsc --build --force",
23
+ "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
24
+ "format": "prettier --write src/"
25
+ },
26
+ "dependencies": {
27
+ "@drax/common-front": "^0.4.0",
28
+ "@drax/common-share": "^0.4.0"
29
+ },
30
+ "peerDependencies": {
31
+ "pinia": "^2.2.2",
32
+ "vue": "^3.5.7",
33
+ "vue-i18n": "^9.14.0",
34
+ "vuetify": "^3.7.2"
35
+ },
36
+ "devDependencies": {
37
+ "@rushstack/eslint-patch": "^1.8.0",
38
+ "@tsconfig/node20": "^20.1.4",
39
+ "@types/jsdom": "^21.1.7",
40
+ "@types/node": "^20.12.5",
41
+ "@vitejs/plugin-vue": "^5.0.4",
42
+ "@vue/eslint-config-prettier": "^9.0.0",
43
+ "@vue/eslint-config-typescript": "^13.0.0",
44
+ "@vue/test-utils": "^2.4.5",
45
+ "@vue/tsconfig": "^0.5.1",
46
+ "cypress": "^13.7.2",
47
+ "eslint": "^8.57.0",
48
+ "eslint-plugin-cypress": "^2.15.1",
49
+ "eslint-plugin-vue": "^9.23.0",
50
+ "jsdom": "^24.0.0",
51
+ "npm-run-all2": "^6.1.2",
52
+ "pinia": "^2.1.7",
53
+ "pinia-plugin-persistedstate": "^3.2.1",
54
+ "prettier": "^3.2.5",
55
+ "start-server-and-test": "^2.0.3",
56
+ "typescript": "~5.4.0",
57
+ "vite": "^5.4.3",
58
+ "vite-plugin-css-injected-by-js": "^3.5.1",
59
+ "vite-plugin-dts": "^3.9.1",
60
+ "vitest": "^1.4.0",
61
+ "vue": "^3.5.3",
62
+ "vue-tsc": "^2.0.11",
63
+ "vuetify": "^3.7.1"
64
+ },
65
+ "gitHead": "481b302fe72f403abf092806ceca540dd2765dfa"
66
+ }
@@ -0,0 +1,124 @@
1
+ import type {IDraxCrud} from "@drax/common-share";
2
+ import type {
3
+ IFields,
4
+ ICrudForm,
5
+ ICrudHeaders,
6
+ ICrudPermissions,
7
+ ICrudRules,
8
+ ICrudField
9
+ } from "./interfaces/IEntityCrud";
10
+
11
+
12
+
13
+ class EntityCrud{
14
+
15
+ name: string = ''
16
+
17
+ constructor() {
18
+ }
19
+
20
+ static get instance(){
21
+ throw new Error('EntityCrud instance not found')
22
+ }
23
+
24
+
25
+ get headers():ICrudHeaders[]{
26
+ return [
27
+ {title: 'ID',key:'_id'},
28
+ ]
29
+ }
30
+
31
+ get permissions(): ICrudPermissions {
32
+ return {
33
+ manage: 'manage', view: 'view', create: 'create', update: 'update', delete: 'delete'
34
+ }
35
+ }
36
+
37
+ get provider(): IDraxCrud<any, any, any>{
38
+ throw new Error('provider not implemented')
39
+ }
40
+
41
+ get fields():IFields{
42
+ return [
43
+ {name: 'id', type: 'string', label: 'ID', default: '' },
44
+ ]
45
+ }
46
+
47
+ get form():ICrudForm{
48
+
49
+ function objectFields(field:ICrudField){
50
+ let value:any = {}
51
+ if(field.objectFields){
52
+ field.objectFields.forEach(subField => {
53
+ if(subField.type === 'object'){
54
+ value[subField.name] = objectFields(subField)
55
+ }else{
56
+ value[subField.name] = subField.default
57
+ }
58
+
59
+ })
60
+ }
61
+ return value
62
+ }
63
+
64
+ const form = this.fields.reduce((acc, field) => {
65
+
66
+ let value = null
67
+ if(field.type === 'object'){
68
+ value = objectFields(field)
69
+ } else if(field.default != undefined){
70
+ value = field.default
71
+ }
72
+
73
+ return {...acc, [field.name]: value }
74
+ }, {})
75
+
76
+ console.log("Form: ", form)
77
+
78
+ return form
79
+
80
+ }
81
+
82
+ get refs():{ [key: string]: EntityCrud }{
83
+ return {}
84
+ }
85
+
86
+ getRef(ref: string):EntityCrud{
87
+ if(!this.refs.hasOwnProperty(ref)) {
88
+ throw new Error("Ref not found: " + ref)
89
+ }
90
+
91
+ return this.refs[ref] as EntityCrud
92
+ }
93
+
94
+ get rules(): ICrudRules{
95
+ return {}
96
+ }
97
+
98
+ get rule() {
99
+ return (field:string) => this.rules[field] || []
100
+ }
101
+
102
+ get isEditable(){
103
+ return true
104
+ }
105
+
106
+ get isCreatable(){
107
+ return true
108
+ }
109
+
110
+ get isDeletable(){
111
+ return true
112
+ }
113
+
114
+ get dialogFullscreen(){
115
+ return false
116
+ }
117
+
118
+
119
+
120
+
121
+ }
122
+
123
+ export default EntityCrud;
124
+ export { EntityCrud }
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import type {PropType} from "vue";
3
+ import EntityCrud from "../EntityCrud";
4
+ import CrudList from "./CrudList.vue";
5
+ import CrudForm from "./CrudForm.vue";
6
+ import CrudNotify from "./CrudNotify.vue";
7
+ import CrudDialog from "./CrudDialog.vue";
8
+ import {useCrud} from "../composables/UseCrud";
9
+
10
+ const {entity} = defineProps({
11
+ entity: {type: Object as PropType<EntityCrud>, required: true},
12
+ })
13
+
14
+ const {
15
+ onCreate, onEdit, onDelete, onCancel, onSubmit,
16
+ operation, dialog, form, formValid, notify, error, message,
17
+ } = useCrud(entity);
18
+
19
+ </script>
20
+
21
+ <template>
22
+ <v-container fluid class="mt-5">
23
+ <v-card>
24
+
25
+ <crud-list
26
+ :entity="entity"
27
+ @create="onCreate"
28
+ @edit="onEdit"
29
+ @delete="onDelete"
30
+ >
31
+ <template v-for="header in entity.headers" :key="header.key" v-slot:[`item.${header.key}`]="{item, value}">
32
+ <slot :name="`item.${header.key}`" v-bind="{item, value}">
33
+ {{ value }}
34
+ </slot>
35
+ </template>
36
+ </crud-list>
37
+ </v-card>
38
+
39
+ <crud-dialog
40
+ v-model="dialog"
41
+ :entity="entity"
42
+ :operation="operation"
43
+ >
44
+
45
+ <slot name="form">
46
+ <crud-form
47
+ v-model="form"
48
+ :entity="entity"
49
+ :error="error"
50
+ :operation="operation"
51
+ :readonly="operation === 'delete'"
52
+ @submit="onSubmit"
53
+ @cancel="onCancel"
54
+ />
55
+ </slot>
56
+
57
+ </crud-dialog>
58
+
59
+ <crud-notify v-model="notify" :message="message"></crud-notify>
60
+ </v-container>
61
+ </template>
62
+
63
+ <style scoped>
64
+
65
+ </style>
@@ -0,0 +1,94 @@
1
+ <script setup lang="ts">
2
+ import {debounce} from "@drax/common-front"
3
+ import type { PropType, Ref} from "vue";
4
+ import {ref, onBeforeMount} from "vue";
5
+ import EntityCrud from "../EntityCrud";
6
+ import type {ICrudField} from "@/interfaces/IEntityCrud";
7
+
8
+ const valueModel = defineModel({type: [String, Array], required: false})
9
+
10
+ const {entity, multiple} = defineProps({
11
+ entity: {type: Object as PropType<EntityCrud>, required: true},
12
+ field: {type: Object as PropType<ICrudField>, required: true},
13
+ multiple: {type: Boolean, default: false},
14
+ chips: {type: Boolean, default: false},
15
+ closableChips: {type: Boolean, default: true},
16
+ clearable: {type: Boolean, default: true},
17
+ label: {type: String},
18
+ itemValue: {type: [String], default: '_id'},
19
+ itemTitle: {type: [String], default: 'name'},
20
+ rules: {type: Array<Function>, default: []},
21
+ errorMessages: {type: Array as PropType<string[]>, default: []},
22
+ })
23
+
24
+ const loading: Ref<boolean> = ref(false)
25
+ const items: Ref<Array<any>> = ref([])
26
+
27
+ const debouncedSearch = debounce(search, 300)
28
+
29
+ onBeforeMount(async () => {
30
+ if(valueModel.value && valueModel.value.length > 0){
31
+ if(multiple && Array.isArray(valueModel.value) ){
32
+ items.value = valueModel.value
33
+ //await findByIds(valueModel.value)
34
+ }else{
35
+ items.value = [valueModel.value]
36
+ //await findByIds([valueModel.value])
37
+ }
38
+
39
+ }
40
+ })
41
+
42
+ async function findByIds(ids: Array<string>) {
43
+ try{
44
+ loading.value = true
45
+ items.value = await entity.provider.findByIds(ids)
46
+ }catch (e){
47
+ console.error(e)
48
+ }finally{
49
+ loading.value = false
50
+ }
51
+ }
52
+
53
+
54
+
55
+
56
+ async function search(value: any) {
57
+ try{
58
+ loading.value = true
59
+ if(!entity.provider.search){
60
+ throw new Error('Provider does not have a search method')
61
+ }
62
+ items.value = await entity.provider.search(value)
63
+ }catch (e){
64
+ console.error(e)
65
+ }finally{
66
+ loading.value = false
67
+ }
68
+
69
+ }
70
+
71
+ </script>
72
+
73
+ <template>
74
+ <v-autocomplete
75
+ v-model="valueModel"
76
+ :label="label ? label : field.label"
77
+ :placeholder="field.label"
78
+ :items="items"
79
+ :multiple="multiple"
80
+ :chips="chips"
81
+ :closable-chips="closableChips"
82
+ :clearable="clearable"
83
+ :item-value="itemValue"
84
+ :item-title="itemTitle"
85
+ :loading="loading"
86
+ :rules="rules"
87
+ :error-messages="errorMessages"
88
+ @update:search="debouncedSearch"
89
+ ></v-autocomplete>
90
+ </template>
91
+
92
+ <style scoped>
93
+
94
+ </style>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import type {TOperation} from "../interfaces/TOperation";
3
+ import type {PropType} from "vue";
4
+ import EntityCrud from "../EntityCrud";
5
+ const dialog = defineModel({type: Boolean, default: false})
6
+
7
+ defineProps({
8
+ entity: {type: Object as PropType<EntityCrud>, required: true},
9
+ operation: {type: String as PropType<TOperation>}
10
+ })
11
+
12
+ defineEmits(
13
+ ['submit', 'close']
14
+ )
15
+
16
+ </script>
17
+
18
+ <template>
19
+ <v-dialog v-model="dialog" :fullscreen="entity.dialogFullscreen">
20
+ <v-card>
21
+ <v-toolbar>
22
+ <v-toolbar-title>{{entity.name}} {{$te('action.'+operation) ? $t('action.'+operation) : operation}}</v-toolbar-title>
23
+ <v-spacer></v-spacer>
24
+ <v-btn icon @click="dialog = false"><v-icon>mdi-close</v-icon></v-btn>
25
+ </v-toolbar>
26
+ <v-card-text>
27
+ <slot></slot>
28
+ </v-card-text>
29
+ </v-card>
30
+
31
+ </v-dialog>
32
+ </template>
33
+
34
+ <style scoped>
35
+
36
+ </style>
@@ -0,0 +1,66 @@
1
+ <script setup lang="ts">
2
+ import type {PropType} from "vue";
3
+ import {ref} from "vue";
4
+ import EntityCrud from "../EntityCrud";
5
+ import CrudFormField from "./CrudFormField.vue";
6
+ import type {TOperation} from "../interfaces/TOperation";
7
+
8
+
9
+ const valueModel = defineModel({type: [Object]})
10
+
11
+
12
+ const {entity} = defineProps({
13
+ entity: {type: Object as PropType<EntityCrud>, required: true},
14
+ operation: {type: String as PropType<TOperation>, required: true},
15
+ readonly: {type: Boolean, default: false},
16
+ error: {type: String, required: false},
17
+ })
18
+
19
+ const valid = ref()
20
+ const formRef = ref()
21
+
22
+ function submit() {
23
+ formRef.value.validate()
24
+ if(valid.value) {
25
+ emit('submit',valueModel.value)
26
+ }
27
+ }
28
+
29
+ function cancel() {
30
+ emit('cancel')
31
+ }
32
+
33
+ const emit = defineEmits(['submit', 'cancel'])
34
+
35
+ </script>
36
+
37
+ <template>
38
+ <v-form v-model="valid" ref="formRef" @submit.prevent >
39
+ <v-card flat>
40
+ <v-card-text v-if="error">
41
+ <v-alert color="error">{{ $te(error) ? $t(error) : error }}</v-alert>
42
+ </v-card-text>
43
+ <v-card-text>
44
+ <template v-for="field in entity.fields" :key="field.name">
45
+ <crud-form-field
46
+ :field="field"
47
+ :entity="entity"
48
+ v-model="valueModel[field.name]"
49
+ />
50
+ </template>
51
+ </v-card-text>
52
+
53
+ <v-card-actions>
54
+ <v-spacer></v-spacer>
55
+ <v-btn variant="text" color="grey" @click="cancel">{{ $t('action.cancel') }}</v-btn>
56
+ <v-btn variant="flat" color="primary" @click="submit">
57
+ {{ operation ? $t('action.' + operation) : $t('action.sent') }}
58
+ </v-btn>
59
+ </v-card-actions>
60
+ </v-card>
61
+ </v-form>
62
+ </template>
63
+
64
+ <style scoped>
65
+
66
+ </style>
@@ -0,0 +1,175 @@
1
+ <script setup lang="ts">
2
+ import {computed} from "vue";
3
+ import type {PropType} from "vue";
4
+ import type {ICrudField} from "../interfaces/IEntityCrud";
5
+ import CrudFormList from "./CrudFormList.vue";
6
+ import CrudAutocomplete from "./CrudAutocomplete.vue";
7
+ import EntityCrud from "@/EntityCrud";
8
+ import {useI18n} from "vue-i18n";
9
+ import {useCrudStore} from "../stores/UseCrudStore";
10
+ import {VDateInput} from 'vuetify/labs/VDateInput'
11
+ const {t, te} = useI18n()
12
+
13
+ const store = useCrudStore()
14
+
15
+ const valueModel = defineModel({type: [String, Number, Boolean, Object, Array], default: false})
16
+
17
+ const {index, entity, field} = defineProps({
18
+ entity: {type: Object as PropType<EntityCrud>, required: true},
19
+ field: {type: Object as PropType<ICrudField>, required: true},
20
+ readonly: {type: Boolean, default: false},
21
+ index: {type: Number, default: 0},
22
+ })
23
+
24
+ const name = computed(() => index > 0 ? `${field.name}_${index}` : field.name)
25
+
26
+ const label = computed(() => {
27
+ const i18n = `${entity.name}.fields.${field.name}`
28
+ return te(i18n) ? t(i18n) : field.label
29
+ })
30
+
31
+ const rules = computed(() => {
32
+ return entity.rule(field.name) as any
33
+ })
34
+
35
+ const inputErrors = computed(() =>
36
+ store.getInputErrors(field.name).map((error: string) => t(te(error) ? t(error) : error))
37
+ )
38
+
39
+ </script>
40
+
41
+ <template>
42
+
43
+ <div v-if="field && field.type">
44
+
45
+ <v-text-field
46
+ v-if="field.type === 'string'"
47
+ type="text"
48
+ :name="name"
49
+ :label="label"
50
+ v-model="valueModel"
51
+ :readonly="readonly"
52
+ :error-messages="inputErrors"
53
+ :rules="rules"
54
+ >
55
+ </v-text-field>
56
+
57
+ <v-text-field
58
+ v-if="field.type === 'number'"
59
+ type="number"
60
+ :name="name"
61
+ :label="label"
62
+ v-model="valueModel"
63
+ :readonly="readonly"
64
+ :error-messages="inputErrors"
65
+ :rules="rules"
66
+ >
67
+ </v-text-field>
68
+
69
+ <v-checkbox
70
+ v-if="field.type === 'boolean'"
71
+ :name="name"
72
+ :label="label"
73
+ v-model="valueModel"
74
+ :readonly="readonly"
75
+ :error-messages="inputErrors"
76
+ :rules="rules"
77
+ >
78
+ </v-checkbox>
79
+
80
+
81
+ <v-date-input
82
+ v-if="field.type === 'date'"
83
+ type="text"
84
+ :name="name"
85
+ :label="label"
86
+ v-model="valueModel"
87
+ :readonly="readonly"
88
+ :error-messages="inputErrors"
89
+ prepend-inner-icon="mdi-calendar"
90
+ prepend-icon=""
91
+ :rules="rules"
92
+ />
93
+
94
+ <crud-autocomplete
95
+ v-if="field.type === 'ref'"
96
+ :entity="entity.getRef(field.ref).instance"
97
+ :field="field"
98
+ v-model="valueModel"
99
+ :label="label"
100
+ :error-messages="inputErrors"
101
+ :rules="rules"
102
+ />
103
+
104
+ <v-card v-if="field.type === 'object'" class="mt-3" variant="flat" border>
105
+
106
+ <v-card-title class="text-h5">{{ field.label }}</v-card-title>
107
+ <v-card-text>
108
+ <crud-form-field
109
+ v-for="oField in field.objectFields"
110
+ :entity="entity"
111
+ :field="oField"
112
+ v-model="valueModel[oField.name]"
113
+ ></crud-form-field>
114
+ </v-card-text>
115
+
116
+ </v-card>
117
+
118
+ <v-combobox
119
+ v-if="field.type === 'array.string'"
120
+ type="text"
121
+ :name="name"
122
+ :label="label"
123
+ v-model="valueModel"
124
+ :multiple="true"
125
+ :chips="true"
126
+ :closable-chips="true"
127
+ :clearable="true"
128
+ :readonly="readonly"
129
+ :error-messages="inputErrors"
130
+ >
131
+ </v-combobox>
132
+
133
+
134
+ <crud-autocomplete
135
+ v-if="field.type === 'array.ref'"
136
+ :entity="entity.getRef(field.ref).instance"
137
+ :field="field"
138
+ v-model="valueModel"
139
+ :multiple="true"
140
+ :chips="true"
141
+ :clearable="true"
142
+ :label="label"
143
+ :error-messages="inputErrors"
144
+ />
145
+
146
+
147
+ <v-combobox
148
+ v-if="field.type === 'array.number'"
149
+ type="number"
150
+ :name="name"
151
+ :label="label"
152
+ v-model="valueModel"
153
+ :multiple="true"
154
+ :chips="true"
155
+ :clearable="true"
156
+ :readonly="readonly"
157
+ :error-messages="inputErrors"
158
+ >
159
+ </v-combobox>
160
+
161
+
162
+ <crud-form-list
163
+ v-if="field.type === 'array.object'"
164
+ :entity="entity"
165
+ :field="field"
166
+ v-model="valueModel"
167
+ :readonly="readonly"
168
+ />
169
+
170
+ </div>
171
+ </template>
172
+
173
+ <style scoped>
174
+
175
+ </style>
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ import type {PropType} from "vue";
3
+ import type {ICrudField} from "../interfaces/IEntityCrud";
4
+ import CrudFormField from "./CrudFormField.vue";
5
+ import EntityCrud from "@/EntityCrud";
6
+
7
+ const valueModel = defineModel({type: Array, default: () => []});
8
+
9
+ const {field} = defineProps({
10
+ entity: {type: Object as PropType<EntityCrud>, required: true},
11
+ field: {type: Object as PropType<ICrudField>, required: true},
12
+ readonly: {type: Boolean, default: false},
13
+ })
14
+
15
+ function newItem() {
16
+ return field.objectFields ? field.objectFields.reduce((acc, field) => ({...acc, [field.name]: field.default }), {}) : []
17
+ }
18
+
19
+ function getField(key: string):ICrudField|undefined {
20
+ return field.objectFields ? field.objectFields.find(field => field.name === key) : undefined;
21
+ }
22
+
23
+ function hasField(key: string):boolean {
24
+ return field.objectFields ? field.objectFields.some(field => field.name === key) : false;
25
+ }
26
+
27
+ function addItem() {
28
+ valueModel.value.push(newItem());
29
+ }
30
+
31
+ function removeItem(index: number) {
32
+ valueModel.value.splice(index, 1);
33
+ }
34
+
35
+ </script>
36
+
37
+ <template>
38
+ <v-card class="mt-3" variant="flat" border>
39
+
40
+ <v-card-title class="text-h5">{{field.label}}</v-card-title>
41
+ <v-card-text>
42
+ <v-row>
43
+ <v-col cols="12" v-for="(item,index) in valueModel" :key="index" class="text-right">
44
+ <v-row dense align="center">
45
+ <v-col cols="11">
46
+ <template v-for="key in Object.keys(item)" :key="key">
47
+ <crud-form-field
48
+ v-if="hasField(key)"
49
+ :entity="entity"
50
+ :field="getField(key)"
51
+ v-model="valueModel[index][key]"
52
+ :readonly="readonly"
53
+ :index="index"
54
+ />
55
+ </template>
56
+
57
+ </v-col>
58
+ <v-col cols="1">
59
+ <v-btn v-if="!readonly" icon @click="removeItem(index)" small class="text-red text--darken-3">
60
+ <v-icon>mdi-close</v-icon>
61
+ </v-btn>
62
+ </v-col>
63
+ </v-row>
64
+ <v-divider></v-divider>
65
+
66
+ </v-col>
67
+ <v-btn icon @click="addItem" class="text-blue text--darken-3">
68
+ <v-icon>mdi-plus</v-icon>
69
+ </v-btn>
70
+
71
+ </v-row>
72
+ </v-card-text>
73
+
74
+
75
+ </v-card>
76
+
77
+ </template>
78
+
79
+ <style scoped>
80
+
81
+ </style>
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ import type {PropType} from 'vue'
3
+ import {useAuth} from '@drax/identity-vue'
4
+ import EntityCrud from "../EntityCrud";
5
+ import CrudSearch from "./CrudSearch.vue";
6
+ import {useCrud} from "../composables/UseCrud";
7
+ import {useI18n} from "vue-i18n";
8
+ const {t, te} = useI18n()
9
+ const {hasPermission} = useAuth()
10
+
11
+ const {entity} = defineProps({
12
+ entity: {type: Object as PropType<EntityCrud>, required: true},
13
+ })
14
+
15
+ const {loading, itemsPerPage, page, sortBy, search, totalItems, items,
16
+ loadItems} = useCrud(entity)
17
+
18
+ const actions = [{title: t('action.actions'),key:'actions', sortable: false, align: 'right'}]
19
+ const tHeaders = entity.headers.map(header => ({...header, title: t(`${entity.name}.fields.${header.title}`)}))
20
+
21
+ const headers = [...tHeaders, ...actions]
22
+
23
+
24
+ defineExpose({
25
+ loadItems
26
+ });
27
+
28
+ </script>
29
+
30
+ <template>
31
+ <v-data-table-server
32
+ class="border"
33
+ v-if="hasPermission(entity.permissions.view)"
34
+ v-model:items-per-page="itemsPerPage"
35
+ :items-per-page-options="[5, 10, 20, 50]"
36
+ v-model:page="page"
37
+ v-model:sort-by="sortBy"
38
+ :headers="headers"
39
+ :items="items"
40
+ :items-length="totalItems"
41
+ :loading="loading"
42
+ :search="search"
43
+ :multi-sort="false"
44
+ item-value="name"
45
+ @update:options="loadItems"
46
+ >
47
+ <template v-slot:top>
48
+ <v-toolbar density="compact" >
49
+ <v-toolbar-title>{{ entity.name }}</v-toolbar-title>
50
+ <v-spacer></v-spacer>
51
+ <v-btn v-if="entity.isCreatable" icon="mdi-plus" class="mr-1" variant="text" color="primary" @click="$emit('create')">
52
+ </v-btn>
53
+ </v-toolbar>
54
+
55
+ <v-card>
56
+ <v-card-text>
57
+ <crud-search v-model="search"></crud-search>
58
+ </v-card-text>
59
+ </v-card>
60
+
61
+ </template>
62
+
63
+
64
+ <template v-for="header in entity.headers" :key="header.key" v-slot:[`item.${header.key}`]="{item, value}">
65
+ <slot :name="`item.${header.key}`" v-bind="{item, value}" >
66
+ {{value}}
67
+ </slot>
68
+ </template>
69
+
70
+
71
+ <template v-slot:item.actions="{item}">
72
+ <v-btn v-if="entity.isEditable && hasPermission(entity.permissions.update)"
73
+ icon="mdi-pencil" variant="text" color="primary"
74
+ @click="$emit('edit', item)">
75
+ </v-btn>
76
+ <v-btn v-if="entity.isDeletable && hasPermission(entity.permissions.delete)"
77
+ icon="mdi-delete" class="mr-1" variant="text" color="red"
78
+ @click="$emit('delete', item)">
79
+ </v-btn>
80
+ </template>
81
+
82
+ </v-data-table-server>
83
+ </template>
84
+
85
+ <style scoped>
86
+
87
+ </style>
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+
3
+ const valueModel = defineModel({type: Boolean, default: false})
4
+
5
+ defineProps({
6
+ message: {type: String},
7
+ color: {type: String, default: 'success' },
8
+ timeout: {type: Number, default: 3000},
9
+ });
10
+ </script>
11
+
12
+ <template>
13
+
14
+ <v-snackbar v-model="valueModel" :timeout="timeout" :color="color" >
15
+ {{message}}
16
+
17
+ <template v-slot:actions>
18
+ <v-btn
19
+ icon="mdi-close"
20
+ variant="text"
21
+ @click="valueModel = false"
22
+ >
23
+ </v-btn>
24
+ </template>
25
+ </v-snackbar>
26
+
27
+ </template>
28
+
29
+ <style scoped>
30
+
31
+ </style>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ const model = defineModel<any>()
3
+ </script>
4
+
5
+ <template>
6
+ <v-text-field v-model="model" hide-details
7
+ density="compact" class="mr-2"
8
+ variant="outlined"
9
+ append-inner-icon="mdi-magnify"
10
+ :label="$t('action.search')"
11
+ single-line clearable @click:clear="() => model = ''"
12
+ />
13
+ </template>
14
+
15
+ <style scoped>
16
+
17
+ </style>
@@ -0,0 +1,150 @@
1
+ import EntityCrud from "../EntityCrud";
2
+ import type {IDraxPaginateResult} from "@drax/common-share";
3
+ import {useCrudStore} from "../stores/UseCrudStore";
4
+ import {computed} from "vue";
5
+ import type {ICrudField} from "@/interfaces/IEntityCrud";
6
+
7
+ export function useCrud(entity: EntityCrud) {
8
+
9
+ const store = useCrudStore()
10
+
11
+ async function loadItems() {
12
+ store.setLoading(true)
13
+ try {
14
+ const r: IDraxPaginateResult<any> = await entity?.provider.paginate({
15
+ page: store.page,
16
+ limit: store.itemsPerPage,
17
+ orderBy: store.sortBy[0]?.key,
18
+ order: store.sortBy[0]?.order,
19
+ search: store.search
20
+ })
21
+ store.setItems(r.items)
22
+ store.setTotalItems(r.total)
23
+ } catch (e) {
24
+ console.error("Error paginating", e)
25
+ } finally {
26
+ store.setLoading(false)
27
+ }
28
+ }
29
+
30
+ function cast(item: any){
31
+
32
+ entity.fields.filter(field => field.type === 'date')
33
+ .forEach(field => {
34
+ if(field.type === 'date'){
35
+ item[field.name] = new Date(item[field.name])
36
+ }
37
+ })
38
+
39
+ return item
40
+ }
41
+
42
+
43
+ function onCreate() {
44
+ store.setOperation("create")
45
+ store.setForm(entity.form)
46
+ store.setDialog(true)
47
+ }
48
+
49
+ function onEdit(item: object) {
50
+ store.setOperation("edit")
51
+ store.setForm(cast({...item}))
52
+ store.setDialog(true)
53
+ }
54
+
55
+ function onDelete(item: object) {
56
+ store.setOperation("delete")
57
+ store.setForm(cast({...item}))
58
+ store.setDialog(true)
59
+ }
60
+
61
+ function onCancel() {
62
+ store.setDialog(false)
63
+ store.setError("")
64
+ store.setInputErrors(null)
65
+ }
66
+
67
+ function onSubmit(formData: any) {
68
+ console.log("formData", formData)
69
+ store.setInputErrors(null)
70
+ switch (store.operation) {
71
+ case "create":
72
+ doCreate(formData)
73
+ break
74
+ case "edit":
75
+ doUpdate(formData)
76
+ break
77
+ case "delete":
78
+ doDelete(formData)
79
+ break
80
+ }
81
+ }
82
+
83
+ async function doCreate(formData: any) {
84
+ try {
85
+ await entity?.provider.create(formData)
86
+ await loadItems()
87
+ store.setDialog(false)
88
+ store.showMessage("Entity created successfully!")
89
+ } catch (e: any) {
90
+ if(e.inputErrors){
91
+ store.setInputErrors(e.inputErrors)
92
+ }
93
+ store.setError(e.message || "An error occurred while creating the entity")
94
+ console.error("Error creating entity", e)
95
+ }
96
+
97
+ }
98
+
99
+ async function doUpdate(formData: any) {
100
+ try {
101
+ await entity?.provider.update(formData._id, formData)
102
+ await loadItems()
103
+ store.setDialog(false)
104
+ store.showMessage("Entity updated successfully!")
105
+ } catch (e: any) {
106
+ console.log("inputErrors", e.inputErrors)
107
+ if(e.inputErrors){
108
+ store.setInputErrors(e.inputErrors)
109
+ }
110
+ store.setError(e.message || "An error occurred while updating the entity")
111
+ console.error("Error updating entity", e)
112
+ }
113
+
114
+ }
115
+
116
+ async function doDelete(formData: any) {
117
+ try {
118
+ await entity?.provider.delete(formData._id)
119
+ await loadItems()
120
+ store.setDialog(false)
121
+ store.showMessage("Entity deleted successfully!")
122
+ } catch (e: any) {
123
+ store.setError(e.message || "An error occurred while deleting the entity")
124
+ console.error("Error updating entity", e)
125
+ }
126
+
127
+ }
128
+
129
+ const dialog = computed({get(){return store.dialog} , set(value){store.setDialog(value)}})
130
+ const operation = computed({get(){return store.operation} , set(value){store.setOperation(value)}})
131
+ const form = computed({get(){return store.form} , set(value){store.setForm(value)}})
132
+ const formValid = computed({get(){return store.formValid} , set(value){store.setFormValid(value)}})
133
+ const notify = computed({get(){return store.notify} , set(value){store.setNotify(value)}})
134
+ const error = computed({get(){return store.error} , set(value){store.setError(value)}})
135
+ const message = computed({get(){return store.message} , set(value){store.setMessage(value)}})
136
+ const loading = computed({get(){return store.loading} , set(value){store.setLoading(value)}})
137
+ const itemsPerPage = computed({get(){return store.itemsPerPage} , set(value){store.setItemsPerPage(value)}})
138
+ const page = computed({get(){return store.page} , set(value){store.setPage(value)}})
139
+ const sortBy = computed({get(){return store.sortBy} , set(value){store.setSortBy(value)}})
140
+ const search = computed({get(){return store.search} , set(value){store.setSearch(value)}})
141
+ const totalItems = computed({get(){return store.totalItems} , set(value){store.setTotalItems(value)}})
142
+ const items = computed({get(){return store.items} , set(value){store.setItems(value)}})
143
+
144
+ return {
145
+ loadItems, onCreate, onEdit, onDelete, onCancel, onSubmit,
146
+ operation, dialog, form, notify, error, message,
147
+ loading, itemsPerPage, page, sortBy, search, totalItems, items
148
+ }
149
+
150
+ }
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ import Crud from "./components/Crud.vue";
2
+ import CrudDialog from "./components/CrudDialog.vue";
3
+ import CrudForm from "./components/CrudForm.vue";
4
+ import CrudFormField from "./components/CrudFormField.vue";
5
+ import CrudFormList from "./components/CrudFormList.vue";
6
+ import CrudList from "./components/CrudList.vue";
7
+ import CrudNotify from "./components/CrudNotify.vue";
8
+ import CrudSearch from "./components/CrudSearch.vue";
9
+ import {useCrudStore} from "./stores/UseCrudStore";
10
+ import {useCrud} from "./composables/UseCrud";
11
+ import {EntityCrud} from "./EntityCrud";
12
+
13
+ import type {IFields, ICrudForm, ICrudHeaders, ICrudPermissions, ICrudRules} from "./interfaces/IEntityCrud";
14
+ export type {IFields, ICrudForm, ICrudHeaders, ICrudPermissions, ICrudRules}
15
+
16
+ export {
17
+ Crud,
18
+ CrudDialog,
19
+ CrudForm,
20
+ CrudFormField,
21
+ CrudFormList,
22
+ CrudList,
23
+ CrudNotify,
24
+ CrudSearch,
25
+ useCrud,
26
+ useCrudStore,
27
+ EntityCrud
28
+
29
+ }
@@ -0,0 +1,33 @@
1
+ interface ICrudHeaders {
2
+ title: string
3
+ key: string
4
+ }
5
+
6
+ interface ICrudRules {
7
+ [key: string]: Array<Function>
8
+ }
9
+
10
+ interface ICrudField {
11
+ name: string
12
+ type: 'string' | 'number' | 'boolean' | 'date' | 'object' | 'ref' | 'array.string' | 'array.number' | 'array.object' | 'array.ref'
13
+ ref?: string
14
+ objectFields?: ICrudField[]
15
+ label: string,
16
+ default: any
17
+ }
18
+
19
+ interface ICrudForm {
20
+ [key: string]: string | number | boolean | Date | null
21
+ }
22
+
23
+ type IFields = ICrudField[]
24
+
25
+ interface ICrudPermissions {
26
+ manage: string
27
+ view: string
28
+ create: string
29
+ update: string
30
+ delete: string
31
+ }
32
+
33
+ export type {ICrudHeaders, ICrudRules, ICrudField, IFields, ICrudForm, ICrudPermissions}
@@ -0,0 +1,6 @@
1
+ type TOperation = "create" | "edit" | "delete" | null
2
+
3
+
4
+ export type {
5
+ TOperation
6
+ }
@@ -0,0 +1,86 @@
1
+ import {defineStore} from "pinia";
2
+ import type {TOperation} from "../interfaces/TOperation";
3
+
4
+ export const useCrudStore = defineStore('CrudStore', {
5
+ state: () => (
6
+ {
7
+ operation: null as TOperation,
8
+ dialog: false as boolean,
9
+ form: {} as any,
10
+ formValid: {} as any,
11
+ notify: false as boolean,
12
+ error: '' as string,
13
+ message: '' as string,
14
+ items: [] as any[],
15
+ totalItems: 0 as number,
16
+ itemsPerPage: 5 as number,
17
+ page: 1 as number,
18
+ search: '' as string,
19
+ sortBy: [] as any[],
20
+ loading: false,
21
+ inputErrors: null
22
+ }
23
+ ),
24
+ getters:{
25
+ getInputErrors(state: any) {
26
+ return (fieldName:string) => {
27
+ if (state.inputErrors && state.inputErrors[fieldName]) {
28
+ return state.inputErrors[fieldName]
29
+ }
30
+ return []
31
+ }
32
+ }
33
+ },
34
+ actions: {
35
+ setOperation(operation: TOperation) {
36
+ this.operation = operation
37
+ },
38
+ setDialog(dialog: boolean) {
39
+ this.dialog = dialog
40
+ },
41
+ setForm(form: any) {
42
+ this.form = form
43
+ },
44
+ setFormValid(formValid: any) {
45
+ this.formValid = formValid
46
+ },
47
+ setError(error: string) {
48
+ this.error = error
49
+ },
50
+ showMessage(message: string) {
51
+ this.message = message
52
+ this.notify = true
53
+ },
54
+ setNotify(notify: boolean) {
55
+ this.notify = notify
56
+ },
57
+ setMessage(message: string) {
58
+ this.message = message
59
+ },
60
+ setItems(items: any[]) {
61
+ this.items = items
62
+ },
63
+ setTotalItems(totalItems: number) {
64
+ this.totalItems = totalItems
65
+ },
66
+ setItemsPerPage(itemsPerPage: number) {
67
+ this.itemsPerPage = itemsPerPage
68
+ },
69
+ setPage(page: number) {
70
+ this.page = page
71
+ },
72
+ setSearch(search: string) {
73
+ this.search = search
74
+ },
75
+ setSortBy(sortBy: any[]) {
76
+ this.sortBy = sortBy
77
+ },
78
+ setLoading(loading: boolean) {
79
+ this.loading = loading
80
+ },
81
+ setInputErrors(inputErrors: any) {
82
+ this.inputErrors = inputErrors
83
+ }
84
+ }
85
+
86
+ })