Merge pull request #342 from frappe/develop

chore: Merge develop to main
This commit is contained in:
Shariq Ansari 2024-09-10 15:42:48 +05:30 committed by GitHub
commit f452a99b6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 108 additions and 45 deletions

View File

@ -51,7 +51,8 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col gap-1 text-base text-gray-800"> <div class="flex flex-col gap-1 text-base leading-5 text-gray-800">
<div>{{ activity.data.subject }}</div>
<div> <div>
<span class="mr-1 text-gray-600"> {{ __('To') }}: </span> <span class="mr-1 text-gray-600"> {{ __('To') }}: </span>
<span>{{ activity.data.recipients }}</span> <span>{{ activity.data.recipients }}</span>
@ -66,12 +67,8 @@
</span> </span>
<span v-if="activity.data.bcc">{{ activity.data.bcc }}</span> <span v-if="activity.data.bcc">{{ activity.data.bcc }}</span>
</div> </div>
<div>
<span class="mr-1 text-gray-600"> {{ __('Subject') }}: </span>
<span>{{ activity.data.subject }}</span>
</div>
</div> </div>
<div class="border-0 border-t my-3.5 border-gray-200" /> <div class="border-0 border-t mt-3 mb-1 border-gray-200" />
<EmailContent :content="activity.data.content" /> <EmailContent :content="activity.data.content" />
<div v-if="activity.data?.attachments?.length" class="flex flex-wrap gap-2"> <div v-if="activity.data?.attachments?.length" class="flex flex-wrap gap-2">
<AttachmentItem <AttachmentItem
@ -108,6 +105,8 @@ function reply(email, reply_all = false) {
if (!email.subject.startsWith('Re:')) { if (!email.subject.startsWith('Re:')) {
editor.subject = `Re: ${email.subject}` editor.subject = `Re: ${email.subject}`
} else {
editor.subject = email.subject
} }
if (reply_all) { if (reply_all) {

View File

@ -4,6 +4,9 @@
{{ __('Send Invites To') }} {{ __('Send Invites To') }}
</h2> </h2>
<div class="flex-1 overflow-y-auto"> <div class="flex-1 overflow-y-auto">
<label class="block text-xs text-gray-600 mb-1.5">
{{ __('Invite by email') }}
</label>
<MultiValueInput <MultiValueInput
v-model="invitees" v-model="invitees"
:validate="validateEmail" :validate="validateEmail"
@ -26,17 +29,17 @@
<ErrorMessage class="mt-2" v-if="error" :message="error" /> <ErrorMessage class="mt-2" v-if="error" :message="error" />
<template v-if="pendingInvitations.data?.length && !invitees.length"> <template v-if="pendingInvitations.data?.length && !invitees.length">
<div <div
class="mt-4 flex items-center justify-between border-b py-2 text-base text-gray-600" class="mt-6 flex items-center justify-between py-4 text-base font-semibold"
> >
<div class="w-4/5">{{ __('Pending Invites') }}</div> <div>{{ __('Pending Invites') }}</div>
</div> </div>
<ul class="divide-y overflow-auto"> <ul class="flex flex-col gap-1">
<li <li
class="flex items-center justify-between py-2" class="flex items-center justify-between px-2 py-1 rounded-lg bg-gray-50"
v-for="user in pendingInvitations.data" v-for="user in pendingInvitations.data"
:key="user.name" :key="user.name"
> >
<div class="w-4/5 text-base"> <div class="text-base">
<span class="text-gray-900"> <span class="text-gray-900">
{{ user.email }} {{ user.email }}
</span> </span>
@ -46,6 +49,7 @@
<Tooltip text="Delete Invitation"> <Tooltip text="Delete Invitation">
<Button <Button
icon="x" icon="x"
variant="ghost"
:loading=" :loading="
pendingInvitations.delete.loading && pendingInvitations.delete.loading &&
pendingInvitations.delete.params.name === user.name pendingInvitations.delete.params.name === user.name

View File

@ -1,5 +1,14 @@
import './index.css' import './index.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createDialog } from './utils/dialogs'
import { initSocket } from './socket'
import router from './router'
import translationPlugin from './translation'
import { posthogPlugin } from './telemetry'
import App from './App.vue'
import { import {
FrappeUI, FrappeUI,
Button, Button,
@ -14,14 +23,6 @@ import {
frappeRequest, frappeRequest,
FeatherIcon, FeatherIcon,
} from 'frappe-ui' } from 'frappe-ui'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createDialog } from './utils/dialogs'
import { initSocket } from './socket'
import router from './router'
import translationPlugin from './translation'
import { posthogPlugin } from './telemetry'
import App from './App.vue'
let globalComponents = { let globalComponents = {
Button, Button,

View File

@ -18,7 +18,9 @@
@click="showAssignmentModal = true" @click="showAssignmentModal = true"
/> />
</component> </component>
<Dropdown :options="statusOptions('deal', updateField)"> <Dropdown
:options="statusOptions('deal', updateField, deal.data._customStatuses)"
>
<template #default="{ open }"> <template #default="{ open }">
<Button <Button
:label="deal.data.status" :label="deal.data.status"
@ -212,7 +214,7 @@
/> />
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<Dropdown :options="contactOptions(contact.name)"> <Dropdown :options="contactOptions(contact)">
<Button <Button
icon="more-horizontal" icon="more-horizontal"
class="text-gray-600" class="text-gray-600"
@ -339,6 +341,7 @@ import {
createToast, createToast,
setupAssignees, setupAssignees,
setupCustomActions, setupCustomActions,
setupCustomStatuses,
errorMessage, errorMessage,
copyToClipboard, copyToClipboard,
} from '@/utils' } from '@/utils'
@ -380,8 +383,7 @@ const deal = createResource({
params: { name: props.dealId }, params: { name: props.dealId },
cache: ['deal', props.dealId], cache: ['deal', props.dealId],
onSuccess: (data) => { onSuccess: (data) => {
setupAssignees(data) let obj = {
setupCustomActions(data, {
doc: data, doc: data,
$dialog, $dialog,
router, router,
@ -389,7 +391,10 @@ const deal = createResource({
createToast, createToast,
deleteDoc: deleteDeal, deleteDoc: deleteDeal,
call, call,
}) }
setupAssignees(data)
setupCustomStatuses(data, obj)
setupCustomActions(data, obj)
}, },
}) })
@ -567,9 +572,9 @@ const _contact = ref({})
function contactOptions(contact) { function contactOptions(contact) {
let options = [ let options = [
{ {
label: __('Delete'), label: __('Remove'),
icon: 'trash-2', icon: 'trash-2',
onClick: () => removeContact(contact), onClick: () => removeContact(contact.name),
}, },
] ]

View File

@ -431,7 +431,7 @@ function parseRows(rows) {
...(deal.deal_owner && getUser(deal.deal_owner)), ...(deal.deal_owner && getUser(deal.deal_owner)),
} }
} else if (row == '_assign') { } else if (row == '_assign') {
let assignees = JSON.parse(deal._assign) || [] let assignees = JSON.parse(deal._assign || '[]')
if (!assignees.length && deal.deal_owner) { if (!assignees.length && deal.deal_owner) {
assignees = [deal.deal_owner] assignees = [deal.deal_owner]
} }

View File

@ -18,7 +18,7 @@
@click="showAssignmentModal = true" @click="showAssignmentModal = true"
/> />
</component> </component>
<Dropdown :options="statusOptions('lead', updateField)"> <Dropdown :options="statusOptions('lead', updateField, lead.data._customStatuses)">
<template #default="{ open }"> <template #default="{ open }">
<Button <Button
:label="lead.data.status" :label="lead.data.status"
@ -308,6 +308,7 @@ import {
createToast, createToast,
setupAssignees, setupAssignees,
setupCustomActions, setupCustomActions,
setupCustomStatuses,
errorMessage, errorMessage,
copyToClipboard, copyToClipboard,
} from '@/utils' } from '@/utils'
@ -354,8 +355,7 @@ const lead = createResource({
params: { name: props.leadId }, params: { name: props.leadId },
cache: ['lead', props.leadId], cache: ['lead', props.leadId],
onSuccess: (data) => { onSuccess: (data) => {
setupAssignees(data) let obj = {
setupCustomActions(data, {
doc: data, doc: data,
$dialog, $dialog,
router, router,
@ -363,7 +363,10 @@ const lead = createResource({
createToast, createToast,
deleteDoc: deleteLead, deleteDoc: deleteLead,
call, call,
}) }
setupAssignees(data)
setupCustomStatuses(data, obj)
setupCustomActions(data, obj)
}, },
}) })

View File

@ -448,7 +448,7 @@ function parseRows(rows) {
...(lead.lead_owner && getUser(lead.lead_owner)), ...(lead.lead_owner && getUser(lead.lead_owner)),
} }
} else if (row == '_assign') { } else if (row == '_assign') {
let assignees = JSON.parse(lead._assign) || [] let assignees = JSON.parse(lead._assign || '[]')
if (!assignees.length && lead.lead_owner) { if (!assignees.length && lead.lead_owner) {
assignees = [lead.lead_owner] assignees = [lead.lead_owner]
} }

View File

@ -9,7 +9,11 @@
</template> </template>
</Breadcrumbs> </Breadcrumbs>
<div class="absolute right-0"> <div class="absolute right-0">
<Dropdown :options="statusOptions('deal', updateField)"> <Dropdown
:options="
statusOptions('deal', updateField, deal.data._customStatuses)
"
>
<template #default="{ open }"> <template #default="{ open }">
<Button <Button
:label="deal.data.status" :label="deal.data.status"
@ -274,7 +278,12 @@ import Section from '@/components/Section.vue'
import SectionFields from '@/components/SectionFields.vue' import SectionFields from '@/components/SectionFields.vue'
import SLASection from '@/components/SLASection.vue' import SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue' import CustomActions from '@/components/CustomActions.vue'
import { createToast, setupAssignees, setupCustomActions } from '@/utils' import {
createToast,
setupAssignees,
setupCustomActions,
setupCustomStatuses,
} from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { organizationsStore } from '@/stores/organizations' import { organizationsStore } from '@/stores/organizations'
@ -313,8 +322,7 @@ const deal = createResource({
params: { name: props.dealId }, params: { name: props.dealId },
cache: ['deal', props.dealId], cache: ['deal', props.dealId],
onSuccess: (data) => { onSuccess: (data) => {
setupAssignees(data) let obj = {
setupCustomActions(data, {
doc: data, doc: data,
$dialog, $dialog,
router, router,
@ -322,7 +330,10 @@ const deal = createResource({
createToast, createToast,
deleteDoc: deleteDeal, deleteDoc: deleteDeal,
call, call,
}) }
setupAssignees(data)
setupCustomStatuses(data, obj)
setupCustomActions(data, obj)
}, },
}) })

View File

@ -9,7 +9,11 @@
</template> </template>
</Breadcrumbs> </Breadcrumbs>
<div class="absolute right-0"> <div class="absolute right-0">
<Dropdown :options="statusOptions('lead', updateField)"> <Dropdown
:options="
statusOptions('lead', updateField, lead.data._customStatuses)
"
>
<template #default="{ open }"> <template #default="{ open }">
<Button <Button
:label="lead.data.status" :label="lead.data.status"
@ -195,7 +199,12 @@ import Section from '@/components/Section.vue'
import SectionFields from '@/components/SectionFields.vue' import SectionFields from '@/components/SectionFields.vue'
import SLASection from '@/components/SLASection.vue' import SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue' import CustomActions from '@/components/CustomActions.vue'
import { createToast, setupAssignees, setupCustomActions } from '@/utils' import {
createToast,
setupAssignees,
setupCustomActions,
setupCustomStatuses,
} from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { contactsStore } from '@/stores/contacts' import { contactsStore } from '@/stores/contacts'
@ -236,8 +245,7 @@ const lead = createResource({
params: { name: props.leadId }, params: { name: props.leadId },
cache: ['lead', props.leadId], cache: ['lead', props.leadId],
onSuccess: (data) => { onSuccess: (data) => {
setupAssignees(data) let obj = {
setupCustomActions(data, {
doc: data, doc: data,
$dialog, $dialog,
router, router,
@ -245,7 +253,10 @@ const lead = createResource({
createToast, createToast,
deleteDoc: deleteLead, deleteDoc: deleteLead,
call, call,
}) }
setupAssignees(data)
setupCustomStatuses(data, obj)
setupCustomActions(data, obj)
}, },
}) })

View File

@ -91,9 +91,17 @@ export const statusesStore = defineStore('crm-statuses', () => {
return communicationStatuses[name] return communicationStatuses[name]
} }
function statusOptions(doctype, action) { function statusOptions(doctype, action, statuses = []) {
let statusesByName = let statusesByName =
doctype == 'deal' ? dealStatusesByName : leadStatusesByName doctype == 'deal' ? dealStatusesByName : leadStatusesByName
if (statuses.length) {
statusesByName = statuses.reduce((acc, status) => {
acc[status] = statusesByName[status]
return acc
}, {})
}
let options = [] let options = []
for (const status in statusesByName) { for (const status in statusesByName) {
options.push({ options.push({

View File

@ -131,6 +131,27 @@ function getActionsFromScript(script, obj) {
return formScript?.actions || [] return formScript?.actions || []
} }
function getStatusFromScript(script, obj) {
let scriptFn = new Function(script + '\nreturn setupForm')()
let formScript = scriptFn(obj)
return formScript?.statuses || []
}
export function setupCustomStatuses(data, obj) {
if (!data._form_script) return []
let statuses = []
if (Array.isArray(data._form_script)) {
data._form_script.forEach((script) => {
statuses = statuses.concat(getStatusFromScript(script, obj))
})
} else {
statuses = getStatusFromScript(data._form_script, data)
}
data._customStatuses = statuses
}
export function setupCustomActions(data, obj) { export function setupCustomActions(data, obj) {
if (!data._form_script) return [] if (!data._form_script) return []