1
0
forked from test/crm

Merge pull request #68 from shariquerik/bulk-edit

fix: Bulk Edit from list view
This commit is contained in:
Shariq Ansari 2024-02-06 18:42:16 +05:30 committed by GitHub
commit cde43d5fed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 400 additions and 29 deletions

View File

@ -341,3 +341,28 @@ def get_assigned_users(doctype, name):
) )
return list(set(assigned_users)) return list(set(assigned_users))
@frappe.whitelist()
def get_fields(doctype: str):
not_allowed_fieldtypes = list(frappe.model.no_value_fields) + ["Read Only"]
fields = frappe.get_meta(doctype).fields
_fields = []
for field in fields:
if (
field.fieldtype not in not_allowed_fieldtypes
and not field.hidden
and not field.read_only
and not field.is_virtual
and field.fieldname
):
_fields.append({
"label": field.label,
"type": field.fieldtype,
"value": field.fieldname,
"options": field.options,
})
return _fields

View File

@ -62,7 +62,15 @@
</ListRowItem> </ListRowItem>
</ListRow> </ListRow>
</ListRows> </ListRows>
<ListSelectBanner /> <ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Button variant="subtle" label="Edit" @click="editValues(selections, unselectAll)">
<template #prefix>
<EditIcon class="h-3 w-3" />
</template>
</Button>
</template>
</ListSelectBanner>
</ListView> </ListView>
<ListFooter <ListFooter
v-if="pageLengthCount" v-if="pageLengthCount"
@ -74,9 +82,18 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
doctype="Contact"
:selectedValues="selectedValues"
@reload="emit('reload')"
/>
</template> </template>
<script setup> <script setup>
import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import { import {
Avatar, Avatar,
ListView, ListView,
@ -87,7 +104,7 @@ import {
ListRowItem, ListRowItem,
ListFooter, ListFooter,
} from 'frappe-ui' } from 'frappe-ui'
import { watch } from 'vue' import { ref, watch } from 'vue'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -108,7 +125,7 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['loadMore', 'updatePageCount']) const emit = defineEmits(['loadMore', 'updatePageCount', 'reload'])
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
@ -116,4 +133,14 @@ watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return if (val === old_value) return
emit('updatePageCount', val) emit('updatePageCount', val)
}) })
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
</script> </script>

View File

@ -85,7 +85,15 @@
</ListRowItem> </ListRowItem>
</ListRow> </ListRow>
</ListRows> </ListRows>
<ListSelectBanner /> <ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Button variant="subtle" label="Edit" @click="editValues(selections, unselectAll)">
<template #prefix>
<EditIcon class="h-3 w-3" />
</template>
</Button>
</template>
</ListSelectBanner>
</ListView> </ListView>
<ListFooter <ListFooter
v-if="pageLengthCount" v-if="pageLengthCount"
@ -97,12 +105,21 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
doctype="CRM Deal"
:selectedValues="selectedValues"
@reload="emit('reload')"
/>
</template> </template>
<script setup> <script setup>
import MultipleAvatar from '@/components/MultipleAvatar.vue' import MultipleAvatar from '@/components/MultipleAvatar.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import { import {
Avatar, Avatar,
ListView, ListView,
@ -113,7 +130,7 @@ import {
ListSelectBanner, ListSelectBanner,
ListFooter, ListFooter,
} from 'frappe-ui' } from 'frappe-ui'
import { watch } from 'vue' import { ref, watch } from 'vue'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -134,7 +151,7 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['loadMore', 'updatePageCount']) const emit = defineEmits(['loadMore', 'updatePageCount', 'reload'])
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
@ -142,4 +159,14 @@ watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return if (val === old_value) return
emit('updatePageCount', val) emit('updatePageCount', val)
}) })
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
</script> </script>

View File

@ -48,12 +48,23 @@
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
<Button <div class="flex gap-2">
theme="red" <Button
variant="subtle" theme="red"
label="Delete" variant="subtle"
@click="deleteEmailTemplate(selections, unselectAll)" label="Delete"
/> @click="deleteEmailTemplate(selections, unselectAll)"
/>
<Button
variant="subtle"
label="Edit"
@click="editValues(selections, unselectAll)"
>
<template #prefix>
<EditIcon class="h-3 w-3" />
</template>
</Button>
</div>
</template> </template>
</ListSelectBanner> </ListSelectBanner>
</ListView> </ListView>
@ -66,8 +77,17 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
doctype="Email Template"
:selectedValues="selectedValues"
@reload="emit('reload')"
/>
</template> </template>
<script setup> <script setup>
import EditIcon from '@/components/Icons/EditIcon.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { import {
ListView, ListView,
@ -79,7 +99,7 @@ import {
ListFooter, ListFooter,
call, call,
} from 'frappe-ui' } from 'frappe-ui'
import { defineModel, watch } from 'vue' import { ref, defineModel, watch } from 'vue'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -100,7 +120,12 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['loadMore', 'updatePageCount', 'showEmailTemplate', 'reload']) const emit = defineEmits([
'loadMore',
'updatePageCount',
'showEmailTemplate',
'reload',
])
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
@ -143,4 +168,14 @@ function deleteEmailTemplate(selections, unselectAll) {
], ],
}) })
} }
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
</script> </script>

View File

@ -94,7 +94,15 @@
</ListRowItem> </ListRowItem>
</ListRow> </ListRow>
</ListRows> </ListRows>
<ListSelectBanner /> <ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Button variant="subtle" label="Edit" @click="editValues(selections, unselectAll)">
<template #prefix>
<EditIcon class="h-3 w-3" />
</template>
</Button>
</template>
</ListSelectBanner>
</ListView> </ListView>
<ListFooter <ListFooter
v-if="pageLengthCount" v-if="pageLengthCount"
@ -106,12 +114,21 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
doctype="CRM Lead"
:selectedValues="selectedValues"
@reload="emit('reload')"
/>
</template> </template>
<script setup> <script setup>
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import MultipleAvatar from '@/components/MultipleAvatar.vue' import MultipleAvatar from '@/components/MultipleAvatar.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import { import {
Avatar, Avatar,
ListView, ListView,
@ -122,7 +139,7 @@ import {
ListRowItem, ListRowItem,
ListFooter, ListFooter,
} from 'frappe-ui' } from 'frappe-ui'
import { watch } from 'vue' import { ref, watch } from 'vue'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -143,7 +160,7 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['loadMore', 'updatePageCount']) const emit = defineEmits(['loadMore', 'updatePageCount', 'reload'])
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
@ -151,4 +168,14 @@ watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return if (val === old_value) return
emit('updatePageCount', val) emit('updatePageCount', val)
}) })
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
</script> </script>

View File

@ -49,7 +49,15 @@
</ListRowItem> </ListRowItem>
</ListRow> </ListRow>
</ListRows> </ListRows>
<ListSelectBanner /> <ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Button variant="subtle" label="Edit" @click="editValues(selections, unselectAll)">
<template #prefix>
<EditIcon class="h-3 w-3" />
</template>
</Button>
</template>
</ListSelectBanner>
</ListView> </ListView>
<ListFooter <ListFooter
class="border-t px-5 py-2" class="border-t px-5 py-2"
@ -60,8 +68,17 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
doctype="CRM Organization"
:selectedValues="selectedValues"
@reload="emit('reload')"
/>
</template> </template>
<script setup> <script setup>
import EditIcon from '@/components/Icons/EditIcon.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import { import {
Avatar, Avatar,
ListView, ListView,
@ -72,7 +89,7 @@ import {
ListRowItem, ListRowItem,
ListFooter, ListFooter,
} from 'frappe-ui' } from 'frappe-ui'
import { watch } from 'vue' import { ref, watch } from 'vue'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -93,7 +110,7 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['loadMore', 'updatePageCount']) const emit = defineEmits(['loadMore', 'updatePageCount', 'reload'])
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
@ -101,4 +118,14 @@ watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return if (val === old_value) return
emit('updatePageCount', val) emit('updatePageCount', val)
}) })
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
</script> </script>

View File

@ -65,12 +65,23 @@
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
<Button <div class="flex gap-2">
theme="red" <Button
variant="subtle" theme="red"
label="Delete" variant="subtle"
@click="deleteTask(selections, unselectAll)" label="Delete"
/> @click="deleteTask(selections, unselectAll)"
/>
<Button
variant="subtle"
label="Edit"
@click="editValues(selections, unselectAll)"
>
<template #prefix>
<EditIcon class="h-3 w-3" />
</template>
</Button>
</div>
</template> </template>
</ListSelectBanner> </ListSelectBanner>
</ListView> </ListView>
@ -83,11 +94,20 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
doctype="CRM Task"
:selectedValues="selectedValues"
@reload="emit('reload')"
/>
</template> </template>
<script setup> <script setup>
import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue' import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue' import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
import CalendarIcon from '@/components/Icons/CalendarIcon.vue' import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import { dateFormat } from '@/utils' import { dateFormat } from '@/utils'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { import {
@ -102,7 +122,7 @@ import {
call, call,
Tooltip, Tooltip,
} from 'frappe-ui' } from 'frappe-ui'
import { defineModel, watch } from 'vue' import { ref, defineModel, watch } from 'vue'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -166,4 +186,14 @@ function deleteTask(selections, unselectAll) {
], ],
}) })
} }
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
</script> </script>

View File

@ -0,0 +1,165 @@
<template>
<Dialog v-model="show" :options="{ title: 'Bulk Edit' }">
<template #body-content>
<div class="mb-4">
<div class="mb-1.5 text-sm text-gray-600">Field</div>
<Autocomplete
:value="field.label"
:options="fields.data"
@change="(e) => changeField(e)"
placeholder="Select Field..."
/>
</div>
<div>
<div class="mb-1.5 text-sm text-gray-600">Value</div>
<component
:is="getValueComponent(field)"
:value="newValue"
size="md"
@change="(v) => updateValue(v)"
placeholder="Value"
/>
</div>
</template>
<template #actions>
<Button
class="w-full"
variant="solid"
@click="updateValues"
:loading="loading"
:label="`Update ${recordCount} Records`"
/>
</template>
</Dialog>
</template>
<script setup>
import DatePicker from '@/components/Controls/DatePicker.vue'
import Link from '@/components/Controls/Link.vue'
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
import { FormControl, call, createResource, TextEditor } from 'frappe-ui'
import { ref, computed, defineModel, onMounted, h } from 'vue'
const typeCheck = ['Check']
const typeLink = ['Link', 'Dynamic Link']
const typeNumber = ['Float', 'Int', 'Currency', 'Percent']
const typeSelect = ['Select']
const typeEditor = ['Text Editor']
const typeDate = ['Date', 'Datetime']
const props = defineProps({
doctype: {
type: String,
required: true,
},
selectedValues: {
type: Array,
required: true,
},
})
const show = defineModel()
const unselectAll = defineModel('unselectAll')
const emit = defineEmits(['reload'])
const fields = createResource({
url: 'crm.api.doc.get_fields',
cache: ['fields', props.doctype],
params: {
doctype: props.doctype,
},
})
onMounted(() => {
if (fields.data?.length) return
fields.fetch()
})
const recordCount = computed(() => props.selectedValues?.size || 0)
const field = ref({
label: '',
type: '',
value: '',
options: '',
})
const newValue = ref('')
const loading = ref(false)
function updateValues() {
let fieldVal = newValue.value
if (field.value.type == 'Check') {
fieldVal = fieldVal == 'Yes' ? 1 : 0
}
loading.value = true
call(
'frappe.desk.doctype.bulk_update.bulk_update.submit_cancel_or_update_docs',
{
doctype: props.doctype,
docnames: Array.from(props.selectedValues),
action: 'update',
data: {
[field.value.value]: fieldVal || null,
},
}
).then(() => {
field.value = {
label: '',
type: '',
value: '',
options: '',
}
newValue.value = ''
loading.value = false
show.value = false
unselectAll.value()
emit('reload')
})
}
function changeField(f) {
newValue.value = ''
if (!f) return
field.value = f
}
function updateValue(v) {
let value = v.target ? v.target.value : v
newValue.value = value
}
function getValueComponent(f) {
const { type, options } = f
if (typeSelect.includes(type) || typeCheck.includes(type)) {
const _options = type == 'Check' ? ['Yes', 'No'] : getSelectOptions(options)
return h(FormControl, {
type: 'select',
options: _options.map((o) => ({
label: o,
value: o,
})),
})
} else if (typeLink.includes(type)) {
if (type == 'Dynamic Link') {
return h(FormControl, { type: 'text' })
}
return h(Link, { class: 'form-control', doctype: options })
} else if (typeNumber.includes(type)) {
return h(FormControl, { type: 'number' })
} else if (typeDate.includes(type)) {
return h(DatePicker)
} else if (typeEditor.includes(type)) {
return h(TextEditor, {
variant: 'outline',
editorClass:
'!prose-sm overflow-auto min-h-[80px] max-h-80 py-1.5 px-2 rounded border border-gray-300 bg-white hover:border-gray-400 hover:shadow-sm focus:bg-white focus:border-gray-500 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400 text-gray-800 transition-colors',
bubbleMenu: true,
content: newValue.value,
})
} else {
return h(FormControl, { type: 'text' })
}
}
</script>

View File

@ -21,6 +21,7 @@
}" }"
@loadMore="() => loadMore++" @loadMore="() => loadMore++"
@updatePageCount="(count) => (updatedPageCount = count)" @updatePageCount="(count) => (updatedPageCount = count)"
@reload="callLogs.reload()"
/> />
<div <div
v-else-if="callLogs.data" v-else-if="callLogs.data"

View File

@ -26,6 +26,7 @@
}" }"
@loadMore="() => loadMore++" @loadMore="() => loadMore++"
@updatePageCount="(count) => (updatedPageCount = count)" @updatePageCount="(count) => (updatedPageCount = count)"
@reload="contacts.reload()"
/> />
<div <div
v-else-if="contacts.data" v-else-if="contacts.data"

View File

@ -26,6 +26,7 @@
}" }"
@loadMore="() => loadMore++" @loadMore="() => loadMore++"
@updatePageCount="(count) => (updatedPageCount = count)" @updatePageCount="(count) => (updatedPageCount = count)"
@reload="deals.reload()"
/> />
<div v-else-if="deals.data" class="flex h-full items-center justify-center"> <div v-else-if="deals.data" class="flex h-full items-center justify-center">
<div <div

View File

@ -27,7 +27,7 @@
@loadMore="() => loadMore++" @loadMore="() => loadMore++"
@updatePageCount="(count) => (updatedPageCount = count)" @updatePageCount="(count) => (updatedPageCount = count)"
@showEmailTemplate="showEmailTemplate" @showEmailTemplate="showEmailTemplate"
@reload="() => emailTemplates.reload()" @reload="emailTemplates.reload()"
/> />
<div <div
v-else-if="emailTemplates.data" v-else-if="emailTemplates.data"

View File

@ -27,6 +27,7 @@
}" }"
@loadMore="() => loadMore++" @loadMore="() => loadMore++"
@updatePageCount="(count) => (updatedPageCount = count)" @updatePageCount="(count) => (updatedPageCount = count)"
@reload="leads.reload()"
/> />
<div v-else-if="leads.data" class="flex h-full items-center justify-center"> <div v-else-if="leads.data" class="flex h-full items-center justify-center">
<div <div

View File

@ -30,6 +30,7 @@
}" }"
@loadMore="() => loadMore++" @loadMore="() => loadMore++"
@updatePageCount="(count) => (updatedPageCount = count)" @updatePageCount="(count) => (updatedPageCount = count)"
@reload="organizations.reload()"
/> />
<div <div
v-else-if="organizations.data" v-else-if="organizations.data"

View File

@ -27,7 +27,7 @@
@loadMore="() => loadMore++" @loadMore="() => loadMore++"
@updatePageCount="(count) => (updatedPageCount = count)" @updatePageCount="(count) => (updatedPageCount = count)"
@showTask="showTask" @showTask="showTask"
@reload="() => tasks.reload()" @reload="tasks.reload()"
/> />
<div v-else-if="tasks.data" class="flex h-full items-center justify-center"> <div v-else-if="tasks.data" class="flex h-full items-center justify-center">
<div <div
@ -86,6 +86,7 @@ const rows = computed(() => {
const showTaskModal = ref(false) const showTaskModal = ref(false)
const task = ref({ const task = ref({
name: '',
title: '', title: '',
description: '', description: '',
assigned_to: '', assigned_to: '',
@ -99,6 +100,7 @@ const task = ref({
function showTask(name) { function showTask(name) {
let t = rows.value?.find((row) => row.name === name) let t = rows.value?.find((row) => row.name === name)
task.value = { task.value = {
name: t.name,
title: t.title, title: t.title,
description: t.description, description: t.description,
assigned_to: t.assigned_to?.email || '', assigned_to: t.assigned_to?.email || '',
@ -113,6 +115,7 @@ function showTask(name) {
function createTask() { function createTask() {
task.value = { task.value = {
name: '',
title: '', title: '',
description: '', description: '',
assigned_to: '', assigned_to: '',