I using example from documentation datatable with crud options.
How to change "New" on-click function?
What is v-slot:activator="{ on, attrs }"?
I need to call api with vuex dispatch.
I want that before edit dialog is opened row will immediately created with ID parameter inside from api.
I added #click, to make call. Is it correct?
<template>
<v-data-table
:headers="headers"
:items="PIATConsignments"
:footer-props="footerProps"
class="elevation-1"
>
<template v-slot:top>
<v-toolbar
flat
>
<v-toolbar-title>Items</v-toolbar-title>
<v-divider
class="mx-4"
inset
vertical
></v-divider>
<v-spacer></v-spacer>
<v-dialog
v-model="dialogEdit"
scrollable
max-width="800px"
>
<template v-slot:activator="{ on, attrs }">
<v-btn
color="primary"
dark
class="mb-2"
v-bind="attrs"
v-on="on"
#click="newItem"
>
New
</v-btn>
</template>
<v-card>
<v-card-title>
<span class="text-h5">{{ formTitle }}</span>
</v-card-title>
<v-card-text style="height: 600px;">
<v-container>
<v-row>
<v-col
cols="12"
sm="12"
md="6"
>
<v-autocomplete
v-model="editedItem.DepartureCountryDetails"
:items="countries"
item-text="ShortCountryName"
label="Departure"
class="input-group--focused"
v-bind:readonly="isReadonly"
return-object
></v-autocomplete>
</v-col>
<v-col
cols="12"
sm="12"
md="6"
>
<v-autocomplete
v-model="editedItem.DestinationCountryDetails"
:items="countries"
item-text="ShortCountryName"
label="Destination"
class="input-group--focused"
v-bind:readonly="isReadonly"
return-object
></v-autocomplete>
</v-col>
</v-row>
<v-row>
<v-col
cols="12"
sm="12"
md="6"
>
<v-text-field
v-model="editedItem.CAInvoiceValueAmount"
label="Amount"
></v-text-field>
</v-col>
<v-col
cols="12"
sm="12"
md="6"
>
<v-text-field
v-model="editedItem.UnifiedGrossMassMeasure"
label="Weight"
></v-text-field>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue darken-1"
text
#click="close"
>
Close
</v-btn>
<v-btn
color="blue darken-1"
text
#click="save"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="dialogDelete" max-width="500px">
<v-card>
<v-card-title class="text-h5">Do you want to delete item?</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text #click="closeDelete">Нет</v-btn>
<v-btn color="blue darken-1" text #click="deleteItemConfirm">Да</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
</v-toolbar>
</template>
<template v-slot:item.actions="{ item }">
<v-icon
small
class="mr-2"
#click="editItem(item)"
>
mdi-pencil
</v-icon>
<v-icon
small
#click="deleteItem(item)"
>
mdi-delete
</v-icon>
</template>
</v-data-table>
</template>
<script>
import modelPIATConsignment from '#/models/form/consignment/piat/PIATConsignment'
import DepartureCountryDetails from './DepartureCountryDetails.vue'
export default {
name: 'PIATConsignmentDetailsTable',
components: {
DepartureCountryDetails,
},
props: {
consignmentForm: { Object },
},
data: function() {
return {
dialogEdit: false,
dialogDelete: false,
headers: [
{ text: 'Departure', value: 'DepartureCountryDetails.ShortCountryName' },
{ text: 'Destination', value: 'DestinationCountryDetails.ShortCountryName' },
{ text: 'Amount', value: 'CAInvoiceValueAmount' },
{ text: 'Weight', value: 'UnifiedGrossMassMeasure' },
{ text: 'Actions', value: 'actions' },
],
footerProps: {
'disable-items-per-page': true,
'items-per-page-options': [],
'items-per-page-all-text': '',
},
PIATConsignments: this.consignmentForm.PIATConsignmentDetails,
editedIndex: -1,
editedItem: modelPIATConsignment,
defaultItem: modelPIATConsignment,
}
},
computed: {
formTitle () { return this.editedIndex === -1 ? 'New' : 'Edit' },
countries() { return this.$store.getters['dict/countries'] },
},
watch: {
dialogEdit (val) { val || this.close() },
dialogDelete (val) { val || this.closeDelete() },
},
methods: {
editItem (item) {
this.editedIndex = this.PIATConsignments.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogEdit = true
},
deleteItem (item) {
this.editedIndex = this.PIATConsignments.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogDelete = true
},
deleteItemConfirm () {
this.PIATConsignments.splice(this.editedIndex, 1)
this.closeDelete()
},
close () {
this.dialogEdit = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
closeDelete () {
this.dialogDelete = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
save () {
if (this.editedIndex > -1) {
Object.assign(this.PIATConsignments[this.editedIndex], this.editedItem)
}
else {
this.PIATConsignments.push(this.editedItem)
}
this.close()
},
}
}
</script>
After hour trying.
Yes. Adding #click event on the button is right way.
After this just need manipulate with "current" row data.
methods: {
...
newItem() {
this.$store.dispatch('our-dispatch-method')
.then(res => {
res.id // For example we created element in our api and response is new ID.
// Now push to the table item list editedItem and set the index
this.PIATConsignments.push(this.editedItem)
this.editedIndex = this.PIATConsignments.indexOf(this.editedItem)
// Set your ID from api inside editedItem
this.editedItem.id = res.id;
})
}
...
}
Related
<v-container class="text-center hyp-container pa-4">
<v-row>
<button #click="toggleForm">Add new</button>
</v-row>
<v-row>
<v-dialog v-model="showConfirmDelete" width="500">
<v-card>
<v-card-title class="headline grey lighten-2">
Confirm
</v-card-title>
<v-card-text>
<v-spacer></v-spacer>
Are you sure you want to delete this target?
</v-card-text>
<v-card-actions>
<v-btn color="secondary" #click="showConfirmDelete = false"
>Cancel</v-btn
>
<v-spacer></v-spacer>
<v-btn color="primary" #click="doDeleteTarget">Delete</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showTargetForm" width="800">
<v-card>
<form #submit="saveTarget">
<v-card-title class="headline grey lighten-2">
Add Slack target
</v-card-title>
<v-card-text>
<v-spacer></v-spacer>
<v-text-field
label="Target Name"
v-model="target.name"
#keyup="modifyTargetName"
></v-text-field>
<v-text-field
label="Webhook URL"
v-model="target.webhook"
></v-text-field>
<v-text-field
label="Slack Channel"
v-model="target.channel"
#keyup="modifyTargetChannel"
></v-text-field>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-btn color="secondary" #click="showTargetForm = false"
>Close</v-btn
>
<v-spacer></v-spacer>
<v-btn color="secondary" #click="target = {}">Clear</v-btn>
<v-btn color="secondary" #click="testTarget">Test</v-btn>
<v-btn color="primary" type="submit">
Save
</v-btn>
</v-card-actions>
</form>
</v-card>
</v-dialog>
</v-row>
<v-row>
<ol>
<li v-for="target in targets" :key="target.id">
<v-card>
<v-card-text>
<v-card-actions>
<v-btn icon #click="toggleShow(target)">
<v-icon>{{
target.show ? "mdi-chevron-up" : "mdi-chevron-down"
}}</v-icon>
</v-btn>
<div class="title">
{{ target.name }}
{{ target.show }}
</div>
</v-card-actions>
<v-expand-transition>
<div v-if="target.show">
<v-divider></v-divider>
<v-card-text>
{{ target.channel }}
edit
delete
<div class="truncate">{{ target.webhook }}</div>
</v-card-text>
</div>
</v-expand-transition>
</v-card-text>
</v-card>
</li>
</ol>
</v-row>
</v-container>
</template>
<script>
export default {
name: "PersonalTargetsSelector",
props: {
disabled: {
type: Boolean,
default: false,
},
},
data: () => ({
showTargetForm: false,
target: {},
targets: [],
showConfirmDelete: false,
targetToDelete: {},
show: {},
}),
async mounted() {},
methods: {
toggleShow(target) {
console.log(target.show);
this.target.show = !Boolean(target.show);
console.log(target.show);
},
handleClickCancel: function () {
this.target = {};
this.showTargetForm = false;
this.handlePageTrackerEvent(
EVENT_CATEGORIES.personalTargets,
EVENT_ACTIONS.clicked,
"Edit Mode - Cancel a Personal Target"
);
},
saveTarget(e) {
e.preventDefault();
console.log("Save target....", this.target);
if (this.target.id) {
// update
this.targets.map(
(t) =>
this.targets.find((t) => t.id === this.target.id) || this.target
);
} else {
// create
this.target.id = Math.random().toString(26).slice(2);
this.targets.push(this.target);
}
this.target = {};
this.showTargetForm = false;
},
editTarget(target) {
this.target = target;
this.showTargetForm = true;
},
testTarget() {
console.log("test target...");
},
confirmDeleteTarget(target) {
this.showConfirmDelete = true;
this.targetToDelete = target;
},
doDeleteTarget() {
this.targets = this.targets.filter(
(t) => t.id !== this.targetToDelete.id
);
this.showConfirmDelete = false;
},
modifyTargetChannel(e) {
const { value } = e.target;
console.log(value);
this.target.channel = value.indexOf("#") === -1 ? "#" + value : value;
},
modifyTargetName(e) {
const { value } = e.target;
this.target.name =
value.indexOf("[Personal]") === -1 ? "[Personal] " + value : value;
},
toggleForm(e) {
e.preventDefault();
this.showTargetForm = !this.showTargetForm;
},
handleClickClose() {
this.forceClose = new Date().toISOString();
},
setConfirmationShow(value) {
this.resetConfirmationShow = value;
},
handlePageTrackerEvent(category, action, name) {
let _paq = (window._paq = window._paq || []);
_paq.push(["trackEvent", category, action, name]);
pageTrackerLogger(
"Page Tracker Event " +
category +
" " +
action +
" " +
name +
" logged."
);
},
},
watch: {
"target.show"(newValue) {
console.log(newValue);
},
},
};
</script>
<style>
.v-application ol {
list-style-type: none;
padding: 0;
}
</style>
the log shows its changing but the UI does not update its value when i put it in {{target.show}}
You're updating target through the props passed to function. Instead:
methods: {
toggleShow(target) {
console.log(target.show);
this.target.show = !Boolean(this.target.show);
console.log(target.show);
},
}
I'm passing an array to a component but the component sees the array as undefined. Here is the parent calling the component...
<FileList ref="files" class="ma-3 pa-0" :passFiles="true" :passedFiles="header.files"></FileList>
Vue devtools sees the array, it is valid. As seen in the screenshot below:
Yet in my created hook in the controller, it shows this.passedFiles as undefined. (this.passFiles, however, shows correctly as true.)
created(){
console.log(this.passFiles,this.passedFiles); //this.passedFiles shows as undefined
window.addEventListener("resize", this.onResize);
},
I dumped the array right before it gets sent to the component, and it is there, see screenshot:
I tried this just to make sure, and it gets passed to the array fine:
:passedFiles="[{0: '1'}]"
I'm pulling my hair out here. Here is the full component, it is long but it shows you everything
<template>
<div>
<div class="text-center pa-10" v-show="loading">
<v-progress-circular
:size="35"
:width="3"
color="primary"
indeterminate
></v-progress-circular>
</div>
<v-data-table
v-show="!loading"
:headers="showActions ? headers : headersRead"
:items="orderedFiles"
:items-per-page="paginate"
:footer-props="{'items-per-page-options':[paginate, 15, 30, 50, 100, -1]}"
:hide-default-footer="oneFileOnly"
class="elevation-1 custom-rounded-box ma-0 pa-0"
ref="aWidth"
:style="forceHeight&&$vuetify.breakpoint.mdAndUp ? 'height:'+forceHeight+'px;' : ''"
>
<template slot="no-data">
<div>There are currently no files here</div>
</template>
<template v-slot:item.description="{item, index}">
<v-row
no-gutters
style="flex-wrap: nowrap;"
>
<v-col
cols="12"
md="11"
class="flex-grow-0 flex-shrink-0"
>
<v-tooltip bottom v-if="item.description">
<template v-slot:activator="{ on, attrs }">
<a
v-if="item.gdoc"
style="text-decoration: none; color: orange;"
v-bind="attrs"
v-on="on"
#click.prevent="gdocDialog = true;editingFile = item"
class="d-block text-truncate"
:style="$vuetify.breakpoint.mdAndUp ? 'max-width:'+aWidth+'px;' : 'max-width:'+bWidth+'px;'"
>
{{item.description}}
</a>
<a
v-else
:href="'/getFile?id='+item.id"
style="text-decoration: none; color: orange;"
v-bind="attrs"
v-on="on"
class="d-block text-truncate"
:style="$vuetify.breakpoint.mdAndUp ? 'max-width:'+aWidth+'px;' : 'max-width:'+bWidth+'px;'"
>
{{item.description}}
</a>
</template>
<span>{{item.file_name_original}}</span>
</v-tooltip>
<div v-else>
<a
v-if="item.gdoc"
style="text-decoration: none; color: orange;"
#click.prevent="gdocDialog = true;editingFile = item"
class="d-block text-truncate"
:style="$vuetify.breakpoint.mdAndUp ? 'max-width:'+aWidth+'px;' : 'max-width:'+bWidth+'px;'"
>
{{item.file_name_original}}
</a>
<a
v-else
:href="'/getFile?id='+item.id"
style="text-decoration: none; color: orange;"
class="d-block text-truncate"
:style="$vuetify.breakpoint.mdAndUp ? 'max-width:'+aWidth+'px;' : 'max-width:'+bWidth+'px;'"
>
{{item.file_name_original}}
</a>
</div>
</v-col>
<v-col
cols="12"
md="1"
style="min-width: 30px; max-width: 30px;"
class="flex-grow-1 flex-shrink-0"
v-show="$vuetify.breakpoint.mdAndUp"
>
<v-edit-dialog
:return-value.sync="item.description"
#save="editFileInline()"
#open="inlineEditOpen(item, index)"
v-if="showActions"
>
<template v-slot:input>
<v-text-field
ref="inline_file"
v-model="editingFile.description"
label="Edit"
single-line
counter
></v-text-field>
</template>
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-icon
small
class="ml-2"
color="orange"
v-bind="attrs"
v-on="on"
width="100%"
>
mdi-pencil
</v-icon>
</template>
<span>Edit the file description</span>
</v-tooltip>
</v-edit-dialog>
</v-col>
</v-row>
</template>
<template v-slot:item.icon="{ item }">
<v-icon
:color="item.icon_color"
>
{{item.icon}}
</v-icon>
</template>
<template v-slot:item.uploaded="{ item }">
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<div v-bind="attrs"
v-on="on">
{{item.date_difference}}
</div>
</template>
<span>
<v-avatar
size="26px"
class="mr-2"
>
<img
:src="'/img/profile-pictures/'+item.user.profile_photo_thumb"
>
</v-avatar>
{{item.pretty_date}} by {{item.user.full_name}}</span>
</v-tooltip>
</template>
<template v-slot:item.actions="{item}" v-if="showActions">
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-icon
small
color="red"
#click="showDeleteDialog(item)"
v-bind="attrs"
v-on="on"
>
mdi-delete
</v-icon>
</template>
<span>Delete</span>
</v-tooltip>
</template>
</v-data-table>
<!-- Upload modal -->
<v-dialog
v-model="fileUploadDialog"
max-width="500px"
width="500px"
:transition="transitionSiteWide()"
persistent
v-if="showActions"
>
<v-card>
<v-progress-linear
indeterminate
color="yellow darken-2"
v-show="fileUploadProcess"
></v-progress-linear>
<v-toolbar
dark
class="primary"
dense
elevation="0"
>
<v-icon class="mr-2">mdi-cloud-upload</v-icon>
<v-toolbar-title class="text">Upload File(s)</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<v-card-text class="pt-3" v-show="!fileUploadGDoc">
<template>
<v-file-input
small-chips
:label="!oneFileOnly ? 'Upload multiple files by clicking here' : 'Click here to upload a file'"
type="file"
ref="files"
:accept="acceptedFiles()"
#change="onFilePicked()"
:key="componentKey"
show-size
counter
:multiple="!oneFileOnly"
:rules="!oneFileOnly ? rules : rulesSingle"
></v-file-input>
</template>
</v-card-text>
<v-card-text class="pt-3" v-show="fileUploadGDoc">
<v-text-field
ref="gdoc_description"
v-model="gdoc_description"
label="Description"
:rules="gdoc_description_rules"
prepend-icon="mdi-pencil"
></v-text-field>
<v-text-field
ref="gdoc_link"
v-model="gdoc_link"
label="Link to your Google Document"
:rules="gdoc"
prepend-icon="mdi-google-drive"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text
#click="ok"
>
Close
</v-btn>
<v-btn
class="primary"
text
v-show="fileUploadButton"
#click="uploadFiles()"
:loading="fileUploadProcess"
>
Upload
</v-btn>
<v-btn
class="primary"
text
v-show="gdocValidated()"
#click="uploadFiles()"
:loading="fileUploadProcess"
>
Attach
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Delete dialog -->
<v-dialog
v-if="showActions"
v-model="deleteFileConfirm"
max-width="400px"
:transition="transitionSiteWide()"
>
<v-card elevation="0">
<v-progress-linear
indeterminate
color="yellow darken-2"
v-show="deleteFileLoading"
></v-progress-linear>
<v-toolbar
dark
class="primary"
dense
elevation="0"
>
<v-icon class="mr-2">mdi-text-box-minus</v-icon>
<v-toolbar-title class="text">{{editingFile.description ? editingFile.description : editingFile.file_name_original}}</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<v-card-text class="pb-0">
<v-container>
<p>Are you sure you want to delete this file?</p>
<p>{{editingFile.description ? editingFile.description : editingFile.file_name_original}}
will be removed from the system.</p>
</v-container>
</v-card-text>
<v-card-actions>
<v-btn
text
#click="deleteFileConfirm = false"
>
Close
</v-btn>
<v-spacer></v-spacer>
<v-btn
class="primary"
text
#click="deleteSet()"
>
Yes, delete this file
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Gdoc dialog -->
<v-dialog
v-model="gdocDialog"
max-width="1000px"
width="100%"
:transition="transitionSiteWide()"
>
<v-card elevation="0">
<v-toolbar
dark
color="teal"
dense
elevation="0"
>
<v-icon class="mr-2">mdi-google-drive</v-icon>
<v-toolbar-title class="text">{{editingFile.description ? editingFile.description : editingFile.file_name_original}}</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<v-card-text class="pa-0">
<iframe ref="gdocIframe" :src="editingFile.file_name_original" :style="'height:'+iframeHeight+'px;width:100%;border:0'"></iframe>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text
#click="gdocDialog = false"
>
Close
</v-btn>
<v-btn
class="primary"
text
link
#click="openGdoc(editingFile.file_name_original);"
>
Open in new window
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
props: {
type: String,
id: [Number, String],
paginate: Number,
fileUploadDialog: Boolean,
fileUploadGDoc: Boolean,
ok: Function,
showActions: Boolean,
forceHeight: { type: Number, default: 0 },
oneFileOnly: Boolean,
passFiles: Boolean,
passedFiles: Array,
},
data () {
return {
headers: [
{
text: '',
align: 'start',
sortable: false,
value: 'icon',
width: '20px'
},
{ text: 'Description', value: 'description', sortable: false, },
{ text: 'Uploaded', value: 'uploaded', width: '150px' },
{ text: 'Actions', value: 'actions', sortable: false, width: '80px', align: 'center' },
],
headersRead: [
{
text: '',
align: 'start',
sortable: false,
value: 'icon',
width: '20px'
},
{ text: 'Description', value: 'description', sortable: false, },
{ text: 'Uploaded', value: 'uploaded', width: '150px' },
],
rules: [
files => !files || !files.some(file => file.size > 20_097_152) || 'Each file cannot exceed 20mb',
],
rulesSingle: [
],
gdoc: [
(value) => !!value || "Required",
(value) => this.isURL(value) || "URL is not valid",
],
gdoc_description_rules: [
(value) => !!value || "Required",
],
files: [],
fileUploadButton: false,
deleteFileConfirmLoading: false,
fileUpload: false,
fileUploadProcess: false,
componentKey: 0,
postFormData: new FormData(),
editingFile: {
description: null,
index: null,
file_name_original: null,
},
deleteFileConfirm : false,
deleteFileLoading: false,
gdoc_link: null,
gdoc_description: null,
gdocDialog: false,
iframeHeight: 0,
aWidth: 0,
loading: true,
}
},
computed:{
orderedFiles: function () {
return _.orderBy(this.files, 'created_at', 'desc')
},
},
watch: {
files: {
immediate: false,
handler(){
if(this.files){
this.total = this.files.length;
this.$emit('totalFiles', this.total);
}else{
this.total = 0;
}
},
},
id: {
immediate: false,
handler(){
this.getFiles()
},
}
},
methods: {
async getFiles(){
this.loading = true;
await axios
.get('/app/files/getFiles?id=' + this.id + '&type=' + this.type)
.then(response => {
if (response.data.length) {
this.files = response.data;
this.$emit('totalFiles', this.files.length);
this.resize();
this.loading = false;
} else {
this.files = [];
this.$emit('totalFiles', 0);
this.loading = false;
}
})
.catch(error => {
this.majorError();
})
.finally()
},
onFilePicked(e) {
this.postFormData = new FormData();
if(this.$refs.files.validate()){
this.fileUploadButton = true;
}
this.postFormData = new FormData();
for(let key in event.target.files){
if(key >= 0){
this.postFormData.append( 'files[]', event.target.files[key]);
}
}
},
async uploadFiles(){
this.fileUploadProcess = true;
this.postFormData.append('type', this.type);
this.postFormData.append('id', this.id);
this.postFormData.append('gdoc', this.fileUploadGDoc);
this.postFormData.append('gdoc_link', this.gdoc_link);
this.postFormData.append('gdoc_description', this.gdoc_description);
const res = await this.callApi('post', '/app/files/uploadFiles', this.postFormData);
if(res.status===200){
this.componentKey++; //reset trick
this.snackbar(res.data.msg,res.data.type);
this.ok();
this.fileUploadProcess = false;
this.gdoc_link = null;
this.gdoc_description = null;
this.$refs.gdoc_link.reset()
this.$refs.gdoc_description.reset()
if(res.data.files){
for (const file of res.data.files) {
this.files.push(file);
}
}
this.resize();
this.fileUploadButton = false;
}else{
this.fileUploadProcess = false;
this.snackbar(res.data.msg, res.data.type);
}
},
inlineEditOpen (item) {
let obj = { ...item, editingIndex: this.files.indexOf(item) }
this.editingFile = obj;
},
async editFileInline(){
const file = Object.assign({}, this.editingFile); //Turn array into object for laravel
const res = await this.callApi('post', '/app/files/updateFile',
{file: file});
if(res.status===201){
this.files[this.editingFile.editingIndex].description = this.editingFile.description;
this.snackbar(this.editingFile.description + " has been edited successfully", 'success');
this.resize();
}else{
if(res.status===422){
for(let i in res.data.errors) {
this.snackbar(res.data.errors[i][0], 'error');
}
}else{
this.snackbar("There has been an error, we don't have any more information for you", 'error');
}
}
},
showDeleteDialog(file){
this.deleteFileConfirm = true;
let obj = { ...file, index: this.files.indexOf(file)}
this.editingFile= obj;
},
async deleteSet(){
this.deleteFileLoading = true;
const res = await this.callApi('post', '/app/files/deleteFile', this.editingFile);
if(res.status===200){
this.files.splice(this.editingFile.index, 1);
this.snackbar("File deleted successfully", 'success');
this.deleteFileConfirm = false;
}else{
if(res.status===422){
this.snackbar(res.data.msg, 'error');
}
}
this.deleteFileLoading = false;
},
gdocValidated(){
if(this.gdoc_link&&this.$refs.gdoc_link.validate()&&this.gdoc_description){
return true;
}
},
openGdoc(url){
window.open(url, '_blank').focus();
},
onResize() {
this.iframeHeight = window.innerHeight - 220;
if(this.showActions){
this.aWidth = this.$refs.aWidth.$el.clientWidth - 355;
this.bWidth = this.$refs.aWidth.$el.clientWidth - 150;
}else{
this.aWidth = this.$refs.aWidth.$el.clientWidth - 270;
this.bWidth = this.$refs.aWidth.$el.clientWidth - 65;
}
},
resize(){
setTimeout(() => window.dispatchEvent(new Event('resize')), 1);
},
},
async mounted(){
if(this.passFiles){
this.files = this.passedFiles;
//console.log(this.passedFiles,this.files)
this.loading = false;
}else{
this.getFiles();
}
this.onResize();
this.resize();
},
created(){
console.log(this.passFiles,this.passedFiles); //this.passedFiles shows as undefined
window.addEventListener("resize", this.onResize);
},
destroyed(){
window.removeEventListener("resize", this.onResize);
this.editingFile = null;
},
}
</script>
What am I missing here?
I'm using vuetify datatable and I fill the table with API- GET Request via Axios.
The problem is when I click Edit Button on the right the placeholder of the textboxes overlap with the selected rows' values.
The issue can be easily understandable from the picture.
Also, when I delete the default headers i.e., (Carbs (g) and Protein (g)) from the code, the actions column is disappearing.. (Picture 2)
Finally, here is the City.vue
<template>
<v-data-table
:headers="headers"
:items="cities"
sort-by="Id"
class="elevation-1"
>
<template v-slot:top>
<v-toolbar
flat
>
<v-toolbar-title>CITY CRUD</v-toolbar-title>
<v-divider
class="mx-4"
inset
vertical
></v-divider>
<v-spacer></v-spacer>
<v-dialog
v-model="dialog"
max-width="500px"
>
<template v-slot:activator="{ on, attrs }">
<v-btn
color="primary"
dark
class="mb-2"
v-bind="attrs"
v-on="on"
>
New City
</v-btn>
</template>
<v-card>
<v-card-title>
<span class="text-h5">{{ formTitle }}</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col
cols="12"
sm="6"
md="4"
>
<v-text-field
v-model="editedItem.Id"
label="Id"
></v-text-field>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<v-text-field
v-model="editedItem.Text"
label="Text"
></v-text-field>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<v-text-field
v-model="editedItem.Description"
label="Description"
></v-text-field>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue darken-1"
text
#click="close"
>
Cancel
</v-btn>
<v-btn
color="blue darken-1"
text
#click="save"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="dialogDelete" max-width="500px">
<v-card>
<v-card-title class="text-h5">Are you sure you want to delete this item?</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text #click="closeDelete">Cancel</v-btn>
<v-btn color="blue darken-1" text #click="deleteItemConfirm">OK</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
</v-toolbar>
</template>
<template v-slot:[`item.actions`]="{ item }">
<v-icon
small
class="mr-2"
#click="editItem(item)"
>
mdi-pencil
</v-icon>
<v-icon
small
#click="deleteItem(item)"
>
mdi-delete
</v-icon>
</template>
<template v-slot:no-data>
<v-btn
color="primary"
#click="initialize"
>
Reset
</v-btn>
</template>
</v-data-table>
</template>
<script>
import axios from 'axios'
export default {
data: () => ({
dialog: false,
dialogDelete: false,
headers: [
{
text: 'Id',
align: 'start',
sortable: false,
value: 'Id',
},
{ text: 'Text', value: 'Text' },
{ text: 'Description', value: 'Description' },
{ text: 'Carbs (g)', value: 'carbs' },
{ text: 'Protein (g)', value: 'protein' },
{ text: 'Actions', value: 'actions', sortable: false },
],
cities: [],
editedIndex: -1,
editedItem: {
Id: 0,
Text: '',
Description: '',
},
defaultItem: {
Id: 0,
Text: '',
Description: '',
},
}),
watch: {
dialog (val) {
val || this.close()
},
dialogDelete (val) {
val || this.closeDelete()
},
},
created () {
this.initialize()
},
methods: {
initialize () {
axios.get("https://localhost:44377/city")
.then((response)=>{
this.cities=response.data;
});
},
editItem (item) {
this.editedIndex = this.cities.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialog = true
},
deleteItem (item) {
this.editedIndex = this.cities.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogDelete = true
},
deleteItemConfirm () {
this.cities.splice(this.editedIndex, 1)
this.closeDelete()
},
close () {
this.dialog = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
closeDelete () {
this.dialogDelete = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
save () {
if (this.editedIndex > -1) {
Object.assign(this.cities[this.editedIndex], this.editedItem)
} else {
this.cities.push(this.editedItem)
}
this.close()
},
},
}
</script>
I need to open a CRUD dialog from a data table component. Both the dialog and data table share the same parent. The data table is reusable but the CRUD dialog is not.
The use case seems very common. An admin page contains a table of data, each row containing an edit button that opens edit dialog.
I've attempted using Vuex below - however this error occurs:
[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'showUserModal' of undefined"
found in
---> <VBtn>
<VSimpleTable>
<VData>
<VDataTable>
<DataTable> at src/components/DataTable.vue
<Settings> at src/views/Settings.vue
<VContent>
<VApp>
<App> at src/App.vue
<Root>
Why is the imported mutator not available and is this a good approach to achieving the common functionality?
I arrived at my current solution using these 2 approaches
https://markus.oberlehner.net/blog/building-a-modal-dialog-with-vue-and-vuex/
https://forum.vuejs.org/t/how-to-trigger-a-modal-component-from-vuex-store/27243/9
UserAdmin.vue
<template>
<v-container fluid >
<DataTable v-bind:rows="allUsers" v-bind:headers="headers" />
<EditUser />
</v-container>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import DataTable from '../components/DataTable';
import EditUser from '../modals/EditUser';
export default {
name: 'UserAdmin',
methods: {
...mapActions(["getUsers"])
},
computed: mapGetters(["allUsers"]),
components: {
DataTable, EditUser
},
data(){
return {
headers: [
{ text: 'Name', value: 'name' },
{ text: 'Username', value: 'email' },
{ text: 'Administrator', value: 'admin' },
{ text: "", value: "controls", sortable: false}
]
}
},
created(){
this.getUsers();
}
}
</script>
DataTable.vue
<template>
<v-data-table
:headers="headers"
:items="rows"
:items-per-page="5"
class="elevation-1"
>
<!-- https://stackoverflow.com/questions/59081299/vuetify-insert-action-button-in-data-table-and-get-row-data -->
<template v-slot:item.controls="props">
<v-btn class="my-2" fab dark x-small color="blue" #click="onButtonClick(props.item.email)">
<v-icon dark>mdi-pencil</v-icon>
</v-btn>
</template>
</v-data-table>
</template>
<script>
import { mapMutations } from "vuex";
export default {
name: "DataTable",
props:["headers", "rows"],
methods: {
...mapMutations(["toggleUserModal"]),
onButtonClick: function(email) {
console.log("clicked: " + email)
this.toggleUserModal();
}
}
}
</script>
EditUser.vue
<template>
<v-row justify="center">
<v-dialog v-model="dialog" persistent max-width="600px" v-show='showUserModal'>
<v-card>
<v-card-title>
<span class="headline">User Profile</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field label="Legal first name*" required></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field label="Legal middle name" hint="example of helper text only on focus"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Legal last name*"
hint="example of persistent helper text"
persistent-hint
required
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field label="Email*" required></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field label="Password*" type="password" required></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-select
:items="['0-17', '18-29', '30-54', '54+']"
label="Age*"
required
></v-select>
</v-col>
<v-col cols="12" sm="6">
<v-autocomplete
:items="['Skiing', 'Ice hockey', 'Soccer', 'Basketball', 'Hockey', 'Reading', 'Writing', 'Coding', 'Basejump']"
label="Interests"
multiple
></v-autocomplete>
</v-col>
</v-row>
</v-container>
<small>*indicates required field</small>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text #click="dialog = false">Close</v-btn>
<v-btn color="blue darken-1" text #click="dialog = false">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-row>
</template>
<script>
export default {
data: () => ({
dialog: false,
}),
computed: {
showUserModal(){
return this.$store.state.showUserModal
}
}
}
</script>
modals.js
const state = {
showUserModal: false
}
const mutations = {
toggleUserModal: () => (this.showUserModal = !this.showUserModal)
}
const getters = {
showUserModal: state => {
return state.showUserModal
}
}
export default {
state,
getters,
mutations
}
New code based on #Anatoly suggestions - everything works except the events emitted from the dialog, ex: onEditUserConfirmed are not picked up in the parent component.
ModalComponent
<template>
<v-row justify="center">
<v-dialog v-model="visible" persistent max-width="600px">
<v-card v-if="user">
<v-card-title>
<span class="headline">User Profile</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="user.name" label="Legal first name*" required></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field label="Legal middle name" hint="example of helper text only on focus"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Legal last name*"
hint="example of persistent helper text"
persistent-hint
required
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field label="Email*" required></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field label="Password*" type="password" required></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-select :items="['0-17', '18-29', '30-54', '54+']" label="Age*" required></v-select>
</v-col>
<v-col cols="12" sm="6">
<v-autocomplete
:items="['Skiing', 'Ice hockey', 'Soccer', 'Basketball', 'Hockey', 'Reading', 'Writing', 'Coding', 'Basejump']"
label="Interests"
multiple
></v-autocomplete>
</v-col>
</v-row>
</v-container>
<small>*indicates required field</small>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text #click="onCancel">Close</v-btn>
<v-btn color="blue darken-1" text #click="onSave">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-row>
</template>
<script>
export default {
name: "EditUser",
props: {
user: Object,
visible: {
type: Boolean,
default: false
}
},
methods: {
onSave() {
console.log('save button gets here...')
this.$emit("onEditUserConfirmed", this.user);
},
onCancel() {
console.log('cancel button gets here...')
this.$emit("onEditUserCancelled");
}
}
};
</script>
Parent Component
<template>
<v-container fluid>
<v-data-table :headers="headers" :items="allUsers" :items-per-page="5" class="elevation-1">
<!-- https://stackoverflow.com/questions/59081299/vuetify-insert-action-button-in-data-table-and-get-row-data -->
<template v-slot:item.controls="props">
<v-btn class="my-2" fab dark x-small color="blue" #click="onEditClick(props.item)">
<v-icon dark>mdi-pencil</v-icon>
</v-btn>
</template>
</v-data-table>
<EditUser
:user="user"
:visible="isDialogVisible"
#confirmed="onEditUserConfirmed"
#cancelled="onEditUserCancelled"
/>
</v-container>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import EditUser from "../modals/EditUser";
export default {
name: "Settings",
data() {
return {
user: null,
isDialogVisible: false,
headers: [
{ text: "Name", value: "name" },
{ text: "Username", value: "email" },
{ text: "Administrator", value: "admin" },
{ text: "", value: "controls", sortable: false }
]
};
},
methods: {
...mapActions(["getUsers"]),
onEditClick: function(user) {
console.log('Editing user: ' + user.email)
this.user = user;
this.isDialogVisible = true;
},
onEditUserConfirmed(user) {
console.log('Saving user: ' + user.email)
this.isDialogVisible = false;
},
onEditUserCancelled () {
this.isDialogVisible = false;
}
},
computed: mapGetters(["allUsers"]),
components: {
EditUser
},
created() {
this.getUsers();
}
};
</script>
I would not recommend using the state for this task. since it not a very complex scenarios. you should use props and events for handling this kind of scenario
just modify the code a bit.
DataTable.vue
<script>
methods: {
onButtonClick: function(email) {
console.log("clicked: " + email)
this.$emit('openDialog') // or use any name here
}
}
</script>
UserAdmin.vue
<template>
<v-container fluid >
<!-- Listen to the event that you are emitting from DataTable.vue -->
<DataTable :rows="allUsers" :headers="headers" #showDialog="editUser = true" />
<!-- Pass that variable as a Prop -->
<EditUser :showDialog="editUser" />
</v-container>
</template>
<script>
....
data: () => ({
headers: [
{ text: 'Name', value: 'name' },
{ text: 'Username', value: 'email' },
{ text: 'Administrator', value: 'admin' },
{ text: "", value: "controls", sortable: false}
],
editUser: false, // a flag to keep the status of modal.
})
....
</script>
EditUser.vue
<script>
export default {
props: {
showDialog: {
type: Boolean,
default: false
}
},
data: () => ({
dialog: false,
}),
mounted() {
this.dialog = this.showDialog
},
watch: {
showDialog() {
if (this.showDialog)
this.dialog = true
}
}
}
</script>
I hope it should work, it worked for me in my scenario. I won't recommend using the Vuex store in this simple single level structure. Vuex should be used in case of some complex data structures where there are deep layers of components.
The code might have some syntax mistakes (do let me know). but i hope i just conveyed the concept
Use an event in a table component to inform a parent component you wish to edit a user (send a selected user in this event).
Catch the event in a parent component, write a user from the event to a prop in data section and pass this prop to a dialog component.
Use a prop to show/hide dialog from a parent component
Use an event to receive edited user after dialog confirmation.
Something like this:
Parent component
<DataTable v-bind:rows="allUsers" v-bind:headers="headers" #onEdit="onEditUser"/>
<EditUser :user="user" :visible="isDialogVisible" #confirmed="onEditUserConfirmed" #cancelled="onEditUserCancelled"/>
...
data: {
return {
// other data
user: null,
isDialogVisible : false
}
},
methods: {
onEditUser (user) {
this.user = user
this.isDialogVisible = true
},
onEditUserConfirmed (user) {
// hide a dialog
this.isDialogVisible = false
// save a user and refresh a table
},
onEditUserCancelled () {
// hide a dialog
this.isDialogVisible = false
}
}
Table component:
// better send a whole user object insteaf of just e-mail prop? It's up to you
#click="onButtonClick(props.item)"
...
methods: {
onButtonClick: function(user) {
this.$emit('onEdit', user)
}
}
Dialog component:
<v-dialog v-model="visible" ...
// render card only if user is passed
<v-card v-if="user">
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="user.firstName" label="Legal first name*" required></v-text-field>
</v-col>
...
<v-btn color="blue darken-1" text #click="onCancel">Close</v-btn>
<v-btn color="blue darken-1" text #click="onSave">Save</v-btn>
...
export default {
props: {
user: {
type: Object
},
visible: {
type: Boolean,
default: false
}
},
...
methods: {
onSave() {
this.$emit('confirmed', this.user)
},
onCancel () {
this.$emit('cancelled')
}
}
}
I would like to kindly ask you about Vuetify and its Data Table. I am using Vuex for state managing. When I open the page Vuex loads all the data from API. It works perfectly. Then I have the Data Table which also works (update, add, delete), but only one column does not... Data Table is filled with GET_CENTERS and the "one column" is an object (center.customer). When I change center.customer.name state is updated, database is updated, but the value in Data Table is not. (I have tried to just create a list of customers on same page and this list changes if :key is set).
CentersTable
<template>
<div>
<v-data-table
:headers="headers"
:items="GET_CENTERS"
sort-by="name"
class="elevation-1"
>
<template v-slot:top>
<v-toolbar flat color="white">
<v-toolbar-title>My CRUD</v-toolbar-title>
<v-divider
class="mx-4"
inset
vertical
></v-divider>
<div class="flex-grow-1"></div>
<v-dialog v-model="dialog" max-width="500px">
<template v-slot:activator="{ on }">
<v-btn color="primary" dark class="mb-2" v-on="on">Nový Záznam</v-btn>
</template>
<v-card>
<v-card-title>
<span class="headline">{{ formTitle }}</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.name" label="Název"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.address" label="Adresa"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.city" label="Město"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.zip" label="PSČ"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.email" label="Email"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.phone" label="Telefon"></v-text-field>
</v-col>
<v-col cols="12" sm="12" md="12">
<v-combobox
v-model="editedItem.customer"
:items="GET_CUSTOMERS"
:search-input.sync="search"
item-text="name"
item-value="_id"
:hide-selected="hideSelected"
label="Zákazník"
persistent-hint
:clearable="clearable"
return-object
>
<template v-if="noData" v-slot:no-data>
<v-list-item>
<v-list-item-content>
<v-list-item-title>
No results matching "<strong>{{ search }}</strong>". Press <kbd>enter</kbd> to create a new one
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</v-combobox>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<div class="flex-grow-1"></div>
<v-btn color="blue darken-1" text #click="close">Cancel</v-btn>
<v-btn color="blue darken-1" text #click="save">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-toolbar>
</template>
<template v-slot:item.action="{ item }">
<v-icon
small
class="mr-2"
#click="editItem(item)"
>
edit
</v-icon>
<v-icon
small
#click="deleteItem(item)"
>
delete
</v-icon>
</template>
</v-data-table>
<template>
<v-list-item v-for="item in GET_CUSTOMERS" :key="item._id">
<v-list-item-content>{{item}}</v-list-item-content>
</v-list-item>
</template>
</div>
</template>
<script>
import {mapActions, mapGetters} from 'vuex'
export default {
name: "CentersTable",
data: () => ({
dialog: false,
editedItem: {},
headers: [
{
text: 'Název',
align: 'left',
sortable: true,
value: 'name',
},
{
text: 'Adresa',
sortable: true,
value: 'address',
},
{
text: 'Město',
sortable: true,
value: 'city',
},
{
text: 'PSČ',
sortable: true,
value: 'zip',
},
{
text: 'Email',
sortable: true,
value: 'email',
},
{
text: 'Telefon',
sortable: true,
value: 'phone',
},
{
text: 'Zákazník',
sortable: true,
value: 'customer.name',
},
{ text: 'Akce', value: 'action', sortable: false, align: 'right' },
],
search: null,
chips: true,
multiple: true,
hideSelected: true,
noData: true,
clearable: false,
}),
watch: {
dialog (val) {
val || this.close()
},
},
computed: {
formTitle() {
return this.editedItem._id === undefined ? 'Nový Záznam' : 'Upravit Záznam'
},
...mapGetters([
'GET_CUSTOMERS',
'GET_CENTERS'
]),
},
methods: {
...mapActions([
'createCenter',
'updateCenter',
'deleteCenter',
]),
editItem (item) {
this.editedItem = Object.assign({}, item)
this.dialog = true
},
deleteItem (item) {
confirm('Opravdu chcete smazat tento záznam?') && this.deleteCenter(item).then(() => {
this.$noty.success('Záznam byl smazán')
}).catch(() => {
this.$noty.alert('Při mazání záznamu došlo k chybě')
})
},
close () {
this.dialog = false
this.editedItem = {}
setTimeout(() => {
}, 300)
},
save () {
if (this.editedItem._id === undefined) {
this.createCenter(this.editedItem).then(() => {
this.$noty.success('Nový zákazník byl vytvořen')
}).catch(() => {
this.$noty.alert('Při vytváření zákazníka došlo k chybě')
})
} else {
this.updateCenter(this.editedItem).then(() => {
this.$noty.success('Záznam byl upraven')
}).catch(() => {
this.$noty.alert('Při ukládání záznamu došlo k chybě')
})
}
this.close()
},
}
}
</script>
<style scoped>
</style>
And Vuex store
import axios from "axios";
import Vue from "vue";
const state = {
customers: []
};
const mutations = {
SET_CUSTOMERS(state, payload) {
state.customers = payload;
},
UPDATE_CUSTOMER(state, payload) {
const customer = state.customers.findIndex(x => x._id === payload._id);
Vue.set(state.customers, customer, payload);
},
ADD_CUSTOMER(state, payload) {
state.customers.push(payload);
},
REMOVE_CUSTOMER(state, payload) {
const customer = state.customers
.map(customer => customer._id)
.indexOf(payload);
state.customers.splice(customer, 1);
}
};
const actions = {
loadCustomers({ commit }) {
axios.get("/api/customers").then(data => {
commit("SET_CUSTOMERS", data.data);
});
},
updateCustomer({ commit }, payload) {
axios.put(`/api/customers/${payload._id}`, payload).then(response => {
commit("UPDATE_CUSTOMER", response.data);
});
},
createCustomer({ commit }, payload) {
axios.post("/api/customers", payload).then(data => {
commit("ADD_CUSTOMER", data.data);
});
},
deleteCustomer({ commit }, payload) {
axios.delete(`/api/customers/${payload._id}`).then(() => {
commit("REMOVE_CUSTOMER", payload);
});
}
};
const getters = {
GET_CUSTOMERS: state => state.customers
};
export default {
state,
mutations,
actions,
getters
};