fix: allow bulk editing from deals listview
This commit is contained in:
parent
204d730fdc
commit
0177b959ca
@ -341,3 +341,28 @@ def get_assigned_users(doctype, name):
|
||||
)
|
||||
|
||||
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
|
||||
@ -85,7 +85,15 @@
|
||||
</ListRowItem>
|
||||
</ListRow>
|
||||
</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>
|
||||
<ListFooter
|
||||
v-if="pageLengthCount"
|
||||
@ -97,12 +105,21 @@
|
||||
}"
|
||||
@loadMore="emit('loadMore')"
|
||||
/>
|
||||
<EditValueModal
|
||||
v-model="showEditModal"
|
||||
v-model:unselectAll="unselectAllAction"
|
||||
doctype="CRM Deal"
|
||||
:selectedValues="selectedValues"
|
||||
@reload="emit('reload')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import EditValueModal from '@/components/Modals/EditValueModal.vue'
|
||||
import {
|
||||
Avatar,
|
||||
ListView,
|
||||
@ -113,7 +130,7 @@ import {
|
||||
ListSelectBanner,
|
||||
ListFooter,
|
||||
} from 'frappe-ui'
|
||||
import { watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
rows: {
|
||||
@ -134,7 +151,7 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['loadMore', 'updatePageCount'])
|
||||
const emit = defineEmits(['loadMore', 'updatePageCount', 'reload'])
|
||||
|
||||
const pageLengthCount = defineModel()
|
||||
|
||||
@ -142,4 +159,14 @@ watch(pageLengthCount, (val, old_value) => {
|
||||
if (val === old_value) return
|
||||
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>
|
||||
|
||||
156
frontend/src/components/Modals/EditValueModal.vue
Normal file
156
frontend/src/components/Modals/EditValueModal.vue
Normal file
@ -0,0 +1,156 @@
|
||||
<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 } 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 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 {
|
||||
return h(FormControl, { type: 'text' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -26,6 +26,7 @@
|
||||
}"
|
||||
@loadMore="() => loadMore++"
|
||||
@updatePageCount="(count) => (updatedPageCount = count)"
|
||||
@reload="deals.reload()"
|
||||
/>
|
||||
<div v-else-if="deals.data" class="flex h-full items-center justify-center">
|
||||
<div
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user