Merge pull request #695 from frappe/develop
chore: Merge develop to main
This commit is contained in:
commit
f020368c9e
File diff suppressed because it is too large
Load Diff
BIN
crm/public/videos/changeDealStatus.mov
Normal file
BIN
crm/public/videos/changeDealStatus.mov
Normal file
Binary file not shown.
BIN
crm/public/videos/convertToDeal.mov
Normal file
BIN
crm/public/videos/convertToDeal.mov
Normal file
Binary file not shown.
@ -1 +1 @@
|
|||||||
Subproject commit b6efd25b2122c2c1f3beeea1702788c5e6e5555f
|
Subproject commit 3423aa5b5c38d3a1b143ae8ab08cbde7360f9a7c
|
||||||
4
frontend/components.d.ts
vendored
4
frontend/components.d.ts
vendored
@ -150,8 +150,7 @@ declare module 'vue' {
|
|||||||
MobileSidebar: typeof import('./src/components/Mobile/MobileSidebar.vue')['default']
|
MobileSidebar: typeof import('./src/components/Mobile/MobileSidebar.vue')['default']
|
||||||
MoneyIcon: typeof import('./src/components/Icons/MoneyIcon.vue')['default']
|
MoneyIcon: typeof import('./src/components/Icons/MoneyIcon.vue')['default']
|
||||||
MultipleAvatar: typeof import('./src/components/MultipleAvatar.vue')['default']
|
MultipleAvatar: typeof import('./src/components/MultipleAvatar.vue')['default']
|
||||||
MultiselectInput: typeof import('./src/components/Controls/MultiselectInput.vue')['default']
|
MultiSelectEmailInput: typeof import('./src/components/Controls/MultiSelectEmailInput.vue')['default']
|
||||||
MultiValueInput: typeof import('./src/components/Controls/MultiValueInput.vue')['default']
|
|
||||||
MuteIcon: typeof import('./src/components/Icons/MuteIcon.vue')['default']
|
MuteIcon: typeof import('./src/components/Icons/MuteIcon.vue')['default']
|
||||||
NestedPopover: typeof import('./src/components/NestedPopover.vue')['default']
|
NestedPopover: typeof import('./src/components/NestedPopover.vue')['default']
|
||||||
NoteArea: typeof import('./src/components/Activities/NoteArea.vue')['default']
|
NoteArea: typeof import('./src/components/Activities/NoteArea.vue')['default']
|
||||||
@ -193,7 +192,6 @@ declare module 'vue' {
|
|||||||
SidePanelLayout: typeof import('./src/components/SidePanelLayout.vue')['default']
|
SidePanelLayout: typeof import('./src/components/SidePanelLayout.vue')['default']
|
||||||
SidePanelLayoutEditor: typeof import('./src/components/SidePanelLayoutEditor.vue')['default']
|
SidePanelLayoutEditor: typeof import('./src/components/SidePanelLayoutEditor.vue')['default']
|
||||||
SidePanelModal: typeof import('./src/components/Modals/SidePanelModal.vue')['default']
|
SidePanelModal: typeof import('./src/components/Modals/SidePanelModal.vue')['default']
|
||||||
SignupBanner: typeof import('./src/components/SignupBanner.vue')['default']
|
|
||||||
SLASection: typeof import('./src/components/SLASection.vue')['default']
|
SLASection: typeof import('./src/components/SLASection.vue')['default']
|
||||||
SmileIcon: typeof import('./src/components/Icons/SmileIcon.vue')['default']
|
SmileIcon: typeof import('./src/components/Icons/SmileIcon.vue')['default']
|
||||||
SortBy: typeof import('./src/components/SortBy.vue')['default']
|
SortBy: typeof import('./src/components/SortBy.vue')['default']
|
||||||
|
|||||||
@ -9,19 +9,15 @@
|
|||||||
"serve": "vite preview"
|
"serve": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tiptap/extension-paragraph": "^2.4.0",
|
|
||||||
"@twilio/voice-sdk": "^2.10.2",
|
"@twilio/voice-sdk": "^2.10.2",
|
||||||
"@vueuse/core": "^10.3.0",
|
|
||||||
"@vueuse/integrations": "^10.3.0",
|
"@vueuse/integrations": "^10.3.0",
|
||||||
"feather-icons": "^4.28.0",
|
"frappe-ui": "^0.1.121",
|
||||||
"frappe-ui": "^0.1.118",
|
|
||||||
"gemoji": "^8.1.0",
|
"gemoji": "^8.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime": "^4.0.1",
|
"mime": "^4.0.1",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
"tailwindcss": "^3.3.3",
|
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.2.2",
|
"vue-router": "^4.2.2",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
@ -31,6 +27,7 @@
|
|||||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
|
"tailwindcss": "^3.4.15",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-pwa": "^0.15.0"
|
"vite-plugin-pwa": "^0.15.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,25 +43,29 @@
|
|||||||
attachment.is_private ? __('Make public') : __('Make private')
|
attachment.is_private ? __('Make public') : __('Make private')
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Button
|
<div>
|
||||||
class="!size-5"
|
<Button
|
||||||
@click.stop="
|
class="!size-5"
|
||||||
togglePrivate(attachment.name, attachment.is_private)
|
@click.stop="
|
||||||
"
|
togglePrivate(attachment.name, attachment.is_private)
|
||||||
>
|
"
|
||||||
<FeatherIcon
|
>
|
||||||
:name="attachment.is_private ? 'lock' : 'unlock'"
|
<FeatherIcon
|
||||||
class="size-3 text-ink-gray-7"
|
:name="attachment.is_private ? 'lock' : 'unlock'"
|
||||||
/>
|
class="size-3 text-ink-gray-7"
|
||||||
</Button>
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip :text="__('Delete attachment')">
|
<Tooltip :text="__('Delete attachment')">
|
||||||
<Button
|
<div>
|
||||||
class="!size-5"
|
<Button
|
||||||
@click.stop="() => deleteAttachment(attachment.name)"
|
class="!size-5"
|
||||||
>
|
@click.stop="() => deleteAttachment(attachment.name)"
|
||||||
<FeatherIcon name="trash-2" class="size-3 text-ink-gray-7" />
|
>
|
||||||
</Button>
|
<FeatherIcon name="trash-2" class="size-3 text-ink-gray-7" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="activity group flex h-48 cursor-pointer flex-col justify-between gap-2 rounded-md bg-surface-menu-bar px-4 py-3 hover:bg-surface-gray-2"
|
class="activity group flex h-48 cursor-pointer flex-col justify-between gap-2 rounded-md bg-surface-gray-1 px-4 py-3 hover:bg-surface-gray-2"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="truncate text-lg font-medium">
|
<div class="truncate text-lg font-medium text-ink-gray-8">
|
||||||
{{ note.title }}
|
{{ note.title }}
|
||||||
</div>
|
</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
|||||||
@ -42,9 +42,11 @@
|
|||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<Tooltip :text="__('Change Status')">
|
<Tooltip :text="__('Change Status')">
|
||||||
<Button variant="ghosted" class="hover:bg-surface-gray-4">
|
<div>
|
||||||
<TaskStatusIcon :status="task.status" />
|
<Button variant="ghosted" class="hover:bg-surface-gray-4">
|
||||||
</Button>
|
<TaskStatusIcon :status="task.status" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
|||||||
@ -175,7 +175,11 @@ const commentEmpty = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const emailEmpty = computed(() => {
|
const emailEmpty = computed(() => {
|
||||||
return !newEmail.value || newEmail.value === '<p></p>'
|
return (
|
||||||
|
!newEmail.value ||
|
||||||
|
newEmail.value === '<p></p>' ||
|
||||||
|
!newEmailEditor.value?.toEmails?.length
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
async function sendMail() {
|
async function sendMail() {
|
||||||
|
|||||||
@ -8,7 +8,10 @@
|
|||||||
:label="value"
|
:label="value"
|
||||||
theme="gray"
|
theme="gray"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
class="rounded"
|
:class="{
|
||||||
|
'rounded bg-surface-white hover:!bg-surface-gray-1 focus-visible:ring-outline-gray-4':
|
||||||
|
variant === 'subtle',
|
||||||
|
}"
|
||||||
@keydown.delete.capture.stop="removeLastValue"
|
@keydown.delete.capture.stop="removeLastValue"
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
@ -25,7 +28,14 @@
|
|||||||
<template #target="{ togglePopover }">
|
<template #target="{ togglePopover }">
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
ref="search"
|
ref="search"
|
||||||
class="search-input form-input w-full border-none bg-surface-white hover:bg-surface-white focus:border-none focus:!shadow-none focus-visible:!ring-0"
|
class="search-input form-input w-full border-none focus:border-none focus:!shadow-none focus-visible:!ring-0"
|
||||||
|
:class="[
|
||||||
|
variant == 'ghost'
|
||||||
|
? 'bg-surface-white hover:bg-surface-white'
|
||||||
|
: 'bg-surface-gray-2 hover:bg-surface-gray-3',
|
||||||
|
inputClass,
|
||||||
|
]"
|
||||||
|
:placeholder="placeholder"
|
||||||
type="text"
|
type="text"
|
||||||
:value="query"
|
:value="query"
|
||||||
@change="
|
@change="
|
||||||
@ -84,6 +94,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" />
|
<ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" />
|
||||||
|
<div
|
||||||
|
v-if="info"
|
||||||
|
class="whitespace-pre-line text-sm text-ink-blue-3 mt-2 pl-2"
|
||||||
|
>
|
||||||
|
{{ info }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -105,6 +121,18 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: 'subtle',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
inputClass: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: (value) => `${value} is an Invalid value`,
|
default: (value) => `${value} is an Invalid value`,
|
||||||
@ -116,6 +144,7 @@ const values = defineModel()
|
|||||||
const emails = ref([])
|
const emails = ref([])
|
||||||
const search = ref(null)
|
const search = ref(null)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
const info = ref(null)
|
||||||
const query = ref('')
|
const query = ref('')
|
||||||
const text = ref('')
|
const text = ref('')
|
||||||
const showOptions = ref(false)
|
const showOptions = ref(false)
|
||||||
@ -181,6 +210,7 @@ function reload(val) {
|
|||||||
|
|
||||||
const addValue = (value) => {
|
const addValue = (value) => {
|
||||||
error.value = null
|
error.value = null
|
||||||
|
info.value = null
|
||||||
if (value) {
|
if (value) {
|
||||||
const splitValues = value.split(',')
|
const splitValues = value.split(',')
|
||||||
splitValues.forEach((value) => {
|
splitValues.forEach((value) => {
|
||||||
@ -191,6 +221,7 @@ const addValue = (value) => {
|
|||||||
// check if value is valid
|
// check if value is valid
|
||||||
if (value && props.validate && !props.validate(value)) {
|
if (value && props.validate && !props.validate(value)) {
|
||||||
error.value = props.errorMessage(value)
|
error.value = props.errorMessage(value)
|
||||||
|
query.value = value
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// add value to values array
|
// add value to values array
|
||||||
@ -200,6 +231,8 @@ const addValue = (value) => {
|
|||||||
values.value.push(value)
|
values.value.push(value)
|
||||||
}
|
}
|
||||||
value = value.replace(value, '')
|
value = value.replace(value, '')
|
||||||
|
} else {
|
||||||
|
info.value = __('email already exists')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -1,126 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="group flex flex-wrap gap-1 min-h-20 p-1.5 cursor-text rounded h-7 text-base bg-surface-gray-2 hover:bg-surface-gray-3 focus:border-outline-gray-4 focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors w-full"
|
|
||||||
@click="setFocus"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
ref="emails"
|
|
||||||
v-for="value in values"
|
|
||||||
:key="value"
|
|
||||||
:label="value"
|
|
||||||
theme="gray"
|
|
||||||
variant="subtle"
|
|
||||||
class="rounded bg-surface-white hover:!bg-surface-gray-1 focus-visible:ring-outline-gray-4"
|
|
||||||
@keydown.delete.capture.stop="removeLastValue"
|
|
||||||
>
|
|
||||||
<template #suffix>
|
|
||||||
<FeatherIcon
|
|
||||||
class="h-3.5"
|
|
||||||
name="x"
|
|
||||||
@click.stop="removeValue(value)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
<div class="flex-1">
|
|
||||||
<input
|
|
||||||
ref="search"
|
|
||||||
class="w-full border-none h-7 text-base bg-surface-gray-2 group-hover:bg-surface-gray-3 focus:border-none focus:!shadow-none focus-visible:!ring-0 transition-colors"
|
|
||||||
type="text"
|
|
||||||
v-model="query"
|
|
||||||
placeholder="example@email.com"
|
|
||||||
@keydown.enter.capture.stop="addValue()"
|
|
||||||
@keydown.delete.capture.stop="removeLastValue"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" />
|
|
||||||
<p v-if="description" class="text-xs text-ink-gray-5 mt-1.5">
|
|
||||||
{{ description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, nextTick } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
validate: {
|
|
||||||
type: Function,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
errorMessage: {
|
|
||||||
type: Function,
|
|
||||||
default: (value) => `${value} is an Invalid value`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const values = defineModel()
|
|
||||||
|
|
||||||
const emails = ref([])
|
|
||||||
const search = ref(null)
|
|
||||||
const error = ref(null)
|
|
||||||
const query = ref('')
|
|
||||||
|
|
||||||
const addValue = () => {
|
|
||||||
let value = query.value
|
|
||||||
error.value = null
|
|
||||||
if (value) {
|
|
||||||
const splitValues = value.split(',')
|
|
||||||
splitValues.forEach((value) => {
|
|
||||||
value = value.trim()
|
|
||||||
if (value) {
|
|
||||||
// check if value is not already in the values array
|
|
||||||
if (!values.value?.includes(value)) {
|
|
||||||
// check if value is valid
|
|
||||||
if (value && props.validate && !props.validate(value)) {
|
|
||||||
error.value = props.errorMessage(value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// add value to values array
|
|
||||||
if (!values.value) {
|
|
||||||
values.value = [value]
|
|
||||||
} else {
|
|
||||||
values.value.push(value)
|
|
||||||
}
|
|
||||||
value = value.replace(value, '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
!error.value && (query.value = '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeValue = (value) => {
|
|
||||||
values.value = values.value.filter((v) => v !== value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeLastValue = () => {
|
|
||||||
if (query.value) return
|
|
||||||
|
|
||||||
let emailRef = emails.value[emails.value.length - 1]?.$el
|
|
||||||
if (document.activeElement === emailRef) {
|
|
||||||
values.value.pop()
|
|
||||||
nextTick(() => {
|
|
||||||
if (values.value.length) {
|
|
||||||
emailRef = emails.value[emails.value.length - 1].$el
|
|
||||||
emailRef?.focus()
|
|
||||||
} else {
|
|
||||||
setFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
emailRef?.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFocus() {
|
|
||||||
search.value.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ setFocus })
|
|
||||||
</script>
|
|
||||||
@ -20,8 +20,9 @@
|
|||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="sm:mx-10 mx-4 flex items-center gap-2 border-t pt-2.5">
|
<div class="sm:mx-10 mx-4 flex items-center gap-2 border-t pt-2.5">
|
||||||
<span class="text-xs text-ink-gray-4">{{ __('TO') }}:</span>
|
<span class="text-xs text-ink-gray-4">{{ __('TO') }}:</span>
|
||||||
<MultiselectInput
|
<MultiSelectEmailInput
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
|
variant="ghost"
|
||||||
v-model="toEmails"
|
v-model="toEmails"
|
||||||
:validate="validateEmail"
|
:validate="validateEmail"
|
||||||
:error-message="
|
:error-message="
|
||||||
@ -53,9 +54,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="cc" class="sm:mx-10 mx-4 flex items-center gap-2">
|
<div v-if="cc" class="sm:mx-10 mx-4 flex items-center gap-2">
|
||||||
<span class="text-xs text-ink-gray-4">{{ __('CC') }}:</span>
|
<span class="text-xs text-ink-gray-4">{{ __('CC') }}:</span>
|
||||||
<MultiselectInput
|
<MultiSelectEmailInput
|
||||||
ref="ccInput"
|
ref="ccInput"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
|
variant="ghost"
|
||||||
v-model="ccEmails"
|
v-model="ccEmails"
|
||||||
:validate="validateEmail"
|
:validate="validateEmail"
|
||||||
:error-message="
|
:error-message="
|
||||||
@ -65,9 +67,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="bcc" class="sm:mx-10 mx-4 flex items-center gap-2">
|
<div v-if="bcc" class="sm:mx-10 mx-4 flex items-center gap-2">
|
||||||
<span class="text-xs text-ink-gray-4">{{ __('BCC') }}:</span>
|
<span class="text-xs text-ink-gray-4">{{ __('BCC') }}:</span>
|
||||||
<MultiselectInput
|
<MultiSelectEmailInput
|
||||||
ref="bccInput"
|
ref="bccInput"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
|
variant="ghost"
|
||||||
v-model="bccEmails"
|
v-model="bccEmails"
|
||||||
:validate="validateEmail"
|
:validate="validateEmail"
|
||||||
:error-message="
|
:error-message="
|
||||||
@ -176,7 +179,7 @@ import SmileIcon from '@/components/Icons/SmileIcon.vue'
|
|||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
||||||
import AttachmentItem from '@/components/AttachmentItem.vue'
|
import AttachmentItem from '@/components/AttachmentItem.vue'
|
||||||
import MultiselectInput from '@/components/Controls/MultiselectInput.vue'
|
import MultiSelectEmailInput from '@/components/Controls/MultiSelectEmailInput.vue'
|
||||||
import EmailTemplateSelectorModal from '@/components/Modals/EmailTemplateSelectorModal.vue'
|
import EmailTemplateSelectorModal from '@/components/Modals/EmailTemplateSelectorModal.vue'
|
||||||
import { TextEditorBubbleMenu, TextEditor, FileUploader, call } from 'frappe-ui'
|
import { TextEditorBubbleMenu, TextEditor, FileUploader, call } from 'frappe-ui'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
|
|||||||
@ -72,14 +72,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2 flex flex-col gap-1">
|
<div class="m-2 flex flex-col gap-1">
|
||||||
<SignupBanner :isSidebarCollapsed="isSidebarCollapsed" />
|
<div class="flex flex-col gap-2 mb-1">
|
||||||
<TrialBanner v-if="isFCSite" :isSidebarCollapsed="isSidebarCollapsed" />
|
<SignupBanner
|
||||||
<GettingStartedBanner
|
v-if="isDemoSite"
|
||||||
v-if="!isOnboardingStepsCompleted"
|
:isSidebarCollapsed="isSidebarCollapsed"
|
||||||
:isSidebarCollapsed="isSidebarCollapsed"
|
:afterSignup="() => capture('signup_from_demo_site')"
|
||||||
/>
|
/>
|
||||||
|
<TrialBanner
|
||||||
|
v-if="isFCSite"
|
||||||
|
:isSidebarCollapsed="isSidebarCollapsed"
|
||||||
|
:afterUpgrade="() => capture('upgrade_plan_from_trial_banner')"
|
||||||
|
/>
|
||||||
|
<GettingStartedBanner
|
||||||
|
v-if="!isOnboardingStepsCompleted"
|
||||||
|
:isSidebarCollapsed="isSidebarCollapsed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
v-else
|
v-if="isOnboardingStepsCompleted"
|
||||||
:label="__('Help')"
|
:label="__('Help')"
|
||||||
:isCollapsed="isSidebarCollapsed"
|
:isCollapsed="isSidebarCollapsed"
|
||||||
@click="
|
@click="
|
||||||
@ -118,9 +128,14 @@
|
|||||||
:logo="CRMLogo"
|
:logo="CRMLogo"
|
||||||
:afterSkip="(step) => capture('onboarding_step_skipped_' + step)"
|
:afterSkip="(step) => capture('onboarding_step_skipped_' + step)"
|
||||||
:afterSkipAll="() => capture('onboarding_steps_skipped')"
|
:afterSkipAll="() => capture('onboarding_steps_skipped')"
|
||||||
:afterReset="() => capture('onboarding_steps_reset')"
|
:afterReset="(step) => capture('onboarding_step_reset_' + step)"
|
||||||
|
:afterResetAll="() => capture('onboarding_steps_reset')"
|
||||||
docsLink="https://docs.frappe.io/crm"
|
docsLink="https://docs.frappe.io/crm"
|
||||||
/>
|
/>
|
||||||
|
<IntermediateStepModal
|
||||||
|
v-model="showIntermediateModal"
|
||||||
|
:currentStep="currentStep"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -148,7 +163,6 @@ import HelpIcon from '@/components/Icons/HelpIcon.vue'
|
|||||||
import SidebarLink from '@/components/SidebarLink.vue'
|
import SidebarLink from '@/components/SidebarLink.vue'
|
||||||
import Notifications from '@/components/Notifications.vue'
|
import Notifications from '@/components/Notifications.vue'
|
||||||
import Settings from '@/components/Settings/Settings.vue'
|
import Settings from '@/components/Settings/Settings.vue'
|
||||||
import SignupBanner from '@/components/SignupBanner.vue'
|
|
||||||
import { viewsStore } from '@/stores/views'
|
import { viewsStore } from '@/stores/views'
|
||||||
import {
|
import {
|
||||||
unreadNotificationsCount,
|
unreadNotificationsCount,
|
||||||
@ -157,12 +171,14 @@ import {
|
|||||||
import { showSettings, activeSettingsPage } from '@/composables/settings'
|
import { showSettings, activeSettingsPage } from '@/composables/settings'
|
||||||
import { FeatherIcon, call } from 'frappe-ui'
|
import { FeatherIcon, call } from 'frappe-ui'
|
||||||
import {
|
import {
|
||||||
|
SignupBanner,
|
||||||
TrialBanner,
|
TrialBanner,
|
||||||
HelpModal,
|
HelpModal,
|
||||||
GettingStartedBanner,
|
GettingStartedBanner,
|
||||||
useOnboarding,
|
useOnboarding,
|
||||||
showHelpModal,
|
showHelpModal,
|
||||||
minimize,
|
minimize,
|
||||||
|
IntermediateStepModal,
|
||||||
} from 'frappe-ui/frappe'
|
} from 'frappe-ui/frappe'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
@ -175,6 +191,7 @@ const { toggle: toggleNotificationPanel } = notificationsStore()
|
|||||||
const isSidebarCollapsed = useStorage('isSidebarCollapsed', false)
|
const isSidebarCollapsed = useStorage('isSidebarCollapsed', false)
|
||||||
|
|
||||||
const isFCSite = ref(window.is_fc_site)
|
const isFCSite = ref(window.is_fc_site)
|
||||||
|
const isDemoSite = ref(window.is_demo_site)
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
@ -284,19 +301,21 @@ function getIcon(routeName, icon) {
|
|||||||
// onboarding
|
// onboarding
|
||||||
const { isOnboardingStepsCompleted, setUp } = useOnboarding('frappecrm')
|
const { isOnboardingStepsCompleted, setUp } = useOnboarding('frappecrm')
|
||||||
|
|
||||||
const firstLead = ref('')
|
|
||||||
const firstDeal = ref('')
|
|
||||||
|
|
||||||
async function getFirstLead() {
|
async function getFirstLead() {
|
||||||
if (firstLead.value) return firstLead.value
|
let firstLead = localStorage.getItem('firstLead')
|
||||||
|
if (firstLead) return firstLead
|
||||||
return await call('crm.api.onboarding.get_first_lead')
|
return await call('crm.api.onboarding.get_first_lead')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFirstDeal() {
|
async function getFirstDeal() {
|
||||||
if (firstDeal.value) return firstDeal.value
|
let firstDeal = localStorage.getItem('firstDeal')
|
||||||
|
if (firstDeal) return firstDeal
|
||||||
return await call('crm.api.onboarding.get_first_deal')
|
return await call('crm.api.onboarding.get_first_deal')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showIntermediateModal = ref(false)
|
||||||
|
const currentStep = ref({})
|
||||||
|
|
||||||
const steps = reactive([
|
const steps = reactive([
|
||||||
{
|
{
|
||||||
name: 'create_first_lead',
|
name: 'create_first_lead',
|
||||||
@ -327,13 +346,23 @@ const steps = reactive([
|
|||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
minimize.value = true
|
minimize.value = true
|
||||||
|
|
||||||
let lead = await getFirstLead()
|
currentStep.value = {
|
||||||
|
title: __('Convert lead to deal'),
|
||||||
|
buttonLabel: __('Convert'),
|
||||||
|
videoURL: '/assets/crm/videos/convertToDeal.mov',
|
||||||
|
onClick: async () => {
|
||||||
|
showIntermediateModal.value = false
|
||||||
|
currentStep.value = {}
|
||||||
|
|
||||||
if (lead) {
|
let lead = await getFirstLead()
|
||||||
router.push({ name: 'Lead', params: { leadId: lead } })
|
if (lead) {
|
||||||
} else {
|
router.push({ name: 'Lead', params: { leadId: lead } })
|
||||||
router.push({ name: 'Leads' })
|
} else {
|
||||||
|
router.push({ name: 'Leads' })
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
showIntermediateModal.value = true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -423,17 +452,28 @@ const steps = reactive([
|
|||||||
completed: false,
|
completed: false,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
minimize.value = true
|
minimize.value = true
|
||||||
let deal = await getFirstDeal()
|
|
||||||
|
|
||||||
if (deal) {
|
currentStep.value = {
|
||||||
router.push({
|
title: __('Change deal status'),
|
||||||
name: 'Deal',
|
buttonLabel: __('Change'),
|
||||||
params: { dealId: deal },
|
videoURL: '/assets/crm/videos/changeDealStatus.mov',
|
||||||
hash: '#activity',
|
onClick: async () => {
|
||||||
})
|
showIntermediateModal.value = false
|
||||||
} else {
|
currentStep.value = {}
|
||||||
router.push({ name: 'Leads' })
|
|
||||||
|
let deal = await getFirstDeal()
|
||||||
|
if (deal) {
|
||||||
|
router.push({
|
||||||
|
name: 'Deal',
|
||||||
|
params: { dealId: deal },
|
||||||
|
hash: '#activity',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'Leads' })
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
showIntermediateModal.value = true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@ -474,6 +514,13 @@ const articles = ref([
|
|||||||
{ name: 'email-template', title: __('Email template') },
|
{ name: 'email-template', title: __('Email template') },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: __('Capturing leads'),
|
||||||
|
opened: false,
|
||||||
|
subArticles: [
|
||||||
|
{ name: 'web-form', title: __('Web form') },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: __('Views'),
|
title: __('Views'),
|
||||||
opened: false,
|
opened: false,
|
||||||
|
|||||||
@ -52,22 +52,21 @@
|
|||||||
v-for="assignee in assignees"
|
v-for="assignee in assignees"
|
||||||
:key="assignee.name"
|
:key="assignee.name"
|
||||||
>
|
>
|
||||||
<Button
|
<div>
|
||||||
:label="getUser(assignee.name).full_name"
|
<Button :label="getUser(assignee.name).full_name" theme="gray">
|
||||||
theme="gray"
|
<template #prefix>
|
||||||
>
|
<UserAvatar :user="assignee.name" size="sm" />
|
||||||
<template #prefix>
|
</template>
|
||||||
<UserAvatar :user="assignee.name" size="sm" />
|
<template #suffix>
|
||||||
</template>
|
<FeatherIcon
|
||||||
<template #suffix>
|
v-if="assignee.name !== owner"
|
||||||
<FeatherIcon
|
class="h-3.5"
|
||||||
v-if="assignee.name !== owner"
|
name="x"
|
||||||
class="h-3.5"
|
@click.stop="removeValue(assignee.name)"
|
||||||
name="x"
|
/>
|
||||||
@click.stop="removeValue(assignee.name)"
|
</template>
|
||||||
/>
|
</Button>
|
||||||
</template>
|
</div>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<ErrorMessage class="mt-2" v-if="error" :message="__(error)" />
|
<ErrorMessage class="mt-2" v-if="error" :message="__(error)" />
|
||||||
|
|||||||
@ -168,7 +168,9 @@ function createNewLead() {
|
|||||||
isLeadCreating.value = false
|
isLeadCreating.value = false
|
||||||
show.value = false
|
show.value = false
|
||||||
router.push({ name: 'Lead', params: { leadId: data.name } })
|
router.push({ name: 'Lead', params: { leadId: data.name } })
|
||||||
updateOnboardingStep('create_first_lead')
|
updateOnboardingStep('create_first_lead', true, false, () => {
|
||||||
|
localStorage.setItem('firstLead', data.name)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
isLeadCreating.value = false
|
isLeadCreating.value = false
|
||||||
|
|||||||
@ -8,14 +8,20 @@
|
|||||||
<label class="block text-xs text-ink-gray-5 mb-1.5">
|
<label class="block text-xs text-ink-gray-5 mb-1.5">
|
||||||
{{ __('Invite by email') }}
|
{{ __('Invite by email') }}
|
||||||
</label>
|
</label>
|
||||||
<MultiValueInput
|
<div
|
||||||
v-model="invitees"
|
class="p-2 group bg-surface-gray-2 hover:bg-surface-gray-3 rounded"
|
||||||
:validate="validateEmail"
|
>
|
||||||
:error-message="
|
<MultiSelectEmailInput
|
||||||
(value) => __('{0} is an invalid email address', [value])
|
class="flex-1"
|
||||||
"
|
inputClass="!bg-surface-gray-2 hover:!bg-surface-gray-3 group-hover:!bg-surface-gray-3"
|
||||||
:description="__('Press enter to add email')"
|
:placeholder="__('john@doe.com')"
|
||||||
/>
|
v-model="invitees"
|
||||||
|
:validate="validateEmail"
|
||||||
|
:error-message="
|
||||||
|
(value) => __('{0} is an invalid email address', [value])
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="select"
|
type="select"
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
@ -51,15 +57,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Tooltip text="Delete Invitation">
|
<Tooltip text="Delete Invitation">
|
||||||
<Button
|
<div>
|
||||||
icon="x"
|
<Button
|
||||||
variant="ghost"
|
icon="x"
|
||||||
:loading="
|
variant="ghost"
|
||||||
pendingInvitations.delete.loading &&
|
:loading="
|
||||||
pendingInvitations.delete.params.name === user.name
|
pendingInvitations.delete.loading &&
|
||||||
"
|
pendingInvitations.delete.params.name === user.name
|
||||||
@click="pendingInvitations.delete.submit(user.name)"
|
"
|
||||||
/>
|
@click="pendingInvitations.delete.submit(user.name)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -72,6 +80,7 @@
|
|||||||
<Button
|
<Button
|
||||||
:label="__('Send Invites')"
|
:label="__('Send Invites')"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
:disabled="!invitees.length"
|
||||||
@click="inviteByEmail.submit()"
|
@click="inviteByEmail.submit()"
|
||||||
:loading="inviteByEmail.loading"
|
:loading="inviteByEmail.loading"
|
||||||
/>
|
/>
|
||||||
@ -79,7 +88,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import MultiValueInput from '@/components/Controls/MultiValueInput.vue'
|
import MultiSelectEmailInput from '@/components/Controls/MultiSelectEmailInput.vue'
|
||||||
import { validateEmail, convertArrayToString } from '@/utils'
|
import { validateEmail, convertArrayToString } from '@/utils'
|
||||||
import {
|
import {
|
||||||
createListResource,
|
createListResource,
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="!isSidebarCollapsed && showBanner"
|
|
||||||
class="m-2 flex flex-col gap-3 shadow-sm rounded-lg py-2.5 px-3 bg-surface-white text-base"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<div class="inline-flex gap-2 items-center font-medium">
|
|
||||||
<FeatherIcon class="h-4" name="info" />
|
|
||||||
{{ __('Loved the demo?') }}
|
|
||||||
</div>
|
|
||||||
<div class="text-ink-gray-7 text-p-sm">
|
|
||||||
{{ __('Try Frappe CRM for free with a 14-day trial.') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button :label="__('Sign up now')" theme="blue" @click="signupNow">
|
|
||||||
<template #prefix>
|
|
||||||
<LightningIcon class="size-4" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import LightningIcon from '@/components/Icons/LightningIcon.vue'
|
|
||||||
import { capture } from '@/telemetry'
|
|
||||||
import { createResource } from 'frappe-ui'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
isSidebarCollapsed: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const showBanner = ref(window.is_demo_site)
|
|
||||||
|
|
||||||
function signupNow() {
|
|
||||||
capture('signup_from_demo_site')
|
|
||||||
window.open('https://frappecloud.com/crm/signup', '_blank')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -2,11 +2,22 @@
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: routeName }"
|
:to="{ name: routeName }"
|
||||||
class="px-0.5 py-1 text-lg font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-5 hover:text-ink-gray-7"
|
class="px-0.5 py-1 text-lg font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-outline-gray-3"
|
||||||
|
:class="[
|
||||||
|
viewControls
|
||||||
|
? 'text-ink-gray-5 hover:text-ink-gray-7'
|
||||||
|
: 'text-ink-gray-7',
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
{{ __(routeName) }}
|
{{ __(routeName) }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<span class="mx-0.5 text-base text-ink-gray-4" aria-hidden="true"> / </span>
|
<span
|
||||||
|
v-if="viewControls"
|
||||||
|
class="mx-0.5 text-base text-ink-gray-4"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
/
|
||||||
|
</span>
|
||||||
<Dropdown v-if="viewControls" :options="viewControls.viewsDropdownOptions">
|
<Dropdown v-if="viewControls" :options="viewControls.viewsDropdownOptions">
|
||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -1,29 +1,24 @@
|
|||||||
@import 'frappe-ui/src/fonts/Inter/inter.css';
|
@import "frappe-ui/src/style.css";
|
||||||
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.prose-f {
|
.prose-f {
|
||||||
@apply
|
@apply break-all
|
||||||
break-all
|
max-w-none
|
||||||
max-w-none
|
prose
|
||||||
prose
|
prose-code:break-all
|
||||||
prose-code:break-all
|
prose-code:whitespace-pre-wrap
|
||||||
prose-code:whitespace-pre-wrap
|
prose-img:border
|
||||||
prose-img:border
|
prose-img:rounded-lg
|
||||||
prose-img:rounded-lg
|
prose-sm
|
||||||
prose-sm
|
prose-table:table-fixed
|
||||||
prose-table:table-fixed
|
prose-td:border
|
||||||
prose-td:border
|
prose-td:border-outline-gray-2
|
||||||
prose-td:border-outline-gray-2
|
prose-td:p-2
|
||||||
prose-td:p-2
|
prose-td:relative
|
||||||
prose-td:relative
|
prose-th:bg-surface-gray-2
|
||||||
prose-th:bg-surface-gray-2
|
prose-th:border
|
||||||
prose-th:border
|
prose-th:border-outline-gray-2
|
||||||
prose-th:border-outline-gray-2
|
prose-th:p-2
|
||||||
prose-th:p-2
|
prose-th:relative;
|
||||||
prose-th:relative
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,38 +75,46 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div class="flex gap-1.5">
|
<div class="flex gap-1.5">
|
||||||
<Tooltip v-if="callEnabled" :text="__('Make a call')">
|
<Tooltip v-if="callEnabled" :text="__('Make a call')">
|
||||||
<Button class="h-7 w-7" @click="triggerCall">
|
<div>
|
||||||
<PhoneIcon class="h-4 w-4" />
|
<Button class="h-7 w-7" @click="triggerCall">
|
||||||
</Button>
|
<PhoneIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip :text="__('Send an email')">
|
<Tooltip :text="__('Send an email')">
|
||||||
<Button class="h-7 w-7">
|
<div>
|
||||||
<Email2Icon
|
<Button class="h-7 w-7">
|
||||||
class="h-4 w-4"
|
<Email2Icon
|
||||||
@click="
|
class="h-4 w-4"
|
||||||
deal.data.email
|
@click="
|
||||||
? openEmailBox()
|
deal.data.email
|
||||||
: errorMessage(__('No email set'))
|
? openEmailBox()
|
||||||
"
|
: errorMessage(__('No email set'))
|
||||||
/>
|
"
|
||||||
</Button>
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip :text="__('Go to website')">
|
<Tooltip :text="__('Go to website')">
|
||||||
<Button class="h-7 w-7">
|
<div>
|
||||||
<LinkIcon
|
<Button class="h-7 w-7">
|
||||||
class="h-4 w-4"
|
<LinkIcon
|
||||||
@click="
|
class="h-4 w-4"
|
||||||
deal.data.website
|
@click="
|
||||||
? openWebsite(deal.data.website)
|
deal.data.website
|
||||||
: errorMessage(__('No website set'))
|
? openWebsite(deal.data.website)
|
||||||
"
|
: errorMessage(__('No website set'))
|
||||||
/>
|
"
|
||||||
</Button>
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip :text="__('Attach a file')">
|
<Tooltip :text="__('Attach a file')">
|
||||||
<Button class="size-7" @click="showFilesUploader = true">
|
<div>
|
||||||
<AttachmentIcon class="size-4" />
|
<Button class="size-7" @click="showFilesUploader = true">
|
||||||
</Button>
|
<AttachmentIcon class="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -117,46 +117,54 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div class="flex gap-1.5">
|
<div class="flex gap-1.5">
|
||||||
<Tooltip v-if="callEnabled" :text="__('Make a call')">
|
<Tooltip v-if="callEnabled" :text="__('Make a call')">
|
||||||
<Button
|
<div>
|
||||||
class="h-7 w-7"
|
<Button
|
||||||
@click="
|
class="h-7 w-7"
|
||||||
() =>
|
@click="
|
||||||
lead.data.mobile_no
|
() =>
|
||||||
? makeCall(lead.data.mobile_no)
|
lead.data.mobile_no
|
||||||
: errorMessage(__('No phone number set'))
|
? makeCall(lead.data.mobile_no)
|
||||||
"
|
: errorMessage(__('No phone number set'))
|
||||||
>
|
"
|
||||||
<PhoneIcon class="h-4 w-4" />
|
>
|
||||||
</Button>
|
<PhoneIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip :text="__('Send an email')">
|
<Tooltip :text="__('Send an email')">
|
||||||
<Button class="h-7 w-7">
|
<div>
|
||||||
<Email2Icon
|
<Button class="h-7 w-7">
|
||||||
class="h-4 w-4"
|
<Email2Icon
|
||||||
@click="
|
class="h-4 w-4"
|
||||||
lead.data.email
|
@click="
|
||||||
? openEmailBox()
|
lead.data.email
|
||||||
: errorMessage(__('No email set'))
|
? openEmailBox()
|
||||||
"
|
: errorMessage(__('No email set'))
|
||||||
/>
|
"
|
||||||
</Button>
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip :text="__('Go to website')">
|
<Tooltip :text="__('Go to website')">
|
||||||
<Button class="h-7 w-7">
|
<div>
|
||||||
<LinkIcon
|
<Button class="h-7 w-7">
|
||||||
class="h-4 w-4"
|
<LinkIcon
|
||||||
@click="
|
class="h-4 w-4"
|
||||||
lead.data.website
|
@click="
|
||||||
? openWebsite(lead.data.website)
|
lead.data.website
|
||||||
: errorMessage(__('No website set'))
|
? openWebsite(lead.data.website)
|
||||||
"
|
: errorMessage(__('No website set'))
|
||||||
/>
|
"
|
||||||
</Button>
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip :text="__('Attach a file')">
|
<Tooltip :text="__('Attach a file')">
|
||||||
<Button class="h-7 w-7" @click="showFilesUploader = true">
|
<div>
|
||||||
<AttachmentIcon class="h-4 w-4" />
|
<Button class="h-7 w-7" @click="showFilesUploader = true">
|
||||||
</Button>
|
<AttachmentIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<ErrorMessage :message="__(error)" />
|
<ErrorMessage :message="__(error)" />
|
||||||
@ -649,7 +657,9 @@ async function convertToDeal() {
|
|||||||
existingOrganizationChecked.value = false
|
existingOrganizationChecked.value = false
|
||||||
existingContact.value = ''
|
existingContact.value = ''
|
||||||
existingOrganization.value = ''
|
existingOrganization.value = ''
|
||||||
updateOnboardingStep('convert_lead_to_deal')
|
updateOnboardingStep('convert_lead_to_deal', true, false, () => {
|
||||||
|
localStorage.setItem('firstDeal', _deal)
|
||||||
|
})
|
||||||
capture('convert_lead_to_deal')
|
capture('convert_lead_to_deal')
|
||||||
router.push({ name: 'Deal', params: { dealId: _deal } })
|
router.push({ name: 'Deal', params: { dealId: _deal } })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,8 +71,9 @@ export default defineConfig({
|
|||||||
'feather-icons',
|
'feather-icons',
|
||||||
'showdown',
|
'showdown',
|
||||||
'tailwind.config.js',
|
'tailwind.config.js',
|
||||||
'engine.io-client',
|
|
||||||
'prosemirror-state',
|
'prosemirror-state',
|
||||||
|
'prosemirror-view',
|
||||||
|
'lowlight',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
18
package.json
18
package.json
@ -1,9 +1,13 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": ["frontend", "frappe-ui"],
|
"workspaces": ["frontend", "frappe-ui"],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "cd frontend && yarn install",
|
"postinstall": "cd frontend && yarn install",
|
||||||
"dev": "cd frontend && yarn dev",
|
"dev": "cd frontend && yarn dev",
|
||||||
"build": "cd frontend && yarn build"
|
"build": "cd frontend && yarn build",
|
||||||
}
|
"disable-workspaces": "sed -i '' 's/\"workspaces\"/\"aworkspaces\"/g' package.json",
|
||||||
|
"enable-workspaces": "sed -i '' 's/\"aworkspaces\"/\"workspaces\"/g' package.json && rm -rf node_modules ./frontend/node_modules/ frappe-ui/node_modules/ && yarn install",
|
||||||
|
"upgrade-frappeui": "cd frontend && yarn add frappe-ui@latest && cd ..",
|
||||||
|
"disable-workspaces-and-upgrade-frappeui": "yarn disable-workspaces && yarn upgrade-frappeui"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
14
yarn.lock
14
yarn.lock
@ -1357,7 +1357,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.10.3.tgz#5ee0feb2f06a59c50e413160840f244215cc4026"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.10.3.tgz#5ee0feb2f06a59c50e413160840f244215cc4026"
|
||||||
integrity sha512-/SFuEDnbJxy3jvi72LeyiPHWkV+uFc0LUHTUHSh20vwyy+tLrzncJfXohGbTIv5YxYhzExQYZDRD4VbSghKdlw==
|
integrity sha512-/SFuEDnbJxy3jvi72LeyiPHWkV+uFc0LUHTUHSh20vwyy+tLrzncJfXohGbTIv5YxYhzExQYZDRD4VbSghKdlw==
|
||||||
|
|
||||||
"@tiptap/extension-paragraph@^2.10.3", "@tiptap/extension-paragraph@^2.4.0":
|
"@tiptap/extension-paragraph@^2.10.3":
|
||||||
version "2.10.3"
|
version "2.10.3"
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.10.3.tgz#128c8fcd46d2e854d214c7f566e6212f2ebff6f1"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.10.3.tgz#128c8fcd46d2e854d214c7f566e6212f2ebff6f1"
|
||||||
integrity sha512-sNkTX/iN+YoleDiTJsrWSBw9D7c4vsYwnW5y/G5ydfuJMIRQMF78pWSIWZFDRNOMkgK5UHkhu9anrbCFYgBfaA==
|
integrity sha512-sNkTX/iN+YoleDiTJsrWSBw9D7c4vsYwnW5y/G5ydfuJMIRQMF78pWSIWZFDRNOMkgK5UHkhu9anrbCFYgBfaA==
|
||||||
@ -1693,7 +1693,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f"
|
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f"
|
||||||
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
|
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
|
||||||
|
|
||||||
"@vueuse/core@10.11.1", "@vueuse/core@^10.11.0", "@vueuse/core@^10.3.0", "@vueuse/core@^10.4.1":
|
"@vueuse/core@10.11.1", "@vueuse/core@^10.11.0", "@vueuse/core@^10.4.1":
|
||||||
version "10.11.1"
|
version "10.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.11.1.tgz#15d2c0b6448d2212235b23a7ba29c27173e0c2c6"
|
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.11.1.tgz#15d2c0b6448d2212235b23a7ba29c27173e0c2c6"
|
||||||
integrity sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==
|
integrity sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==
|
||||||
@ -2542,10 +2542,10 @@ fraction.js@^4.3.7:
|
|||||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||||
|
|
||||||
frappe-ui@^0.1.118:
|
frappe-ui@^0.1.121:
|
||||||
version "0.1.118"
|
version "0.1.121"
|
||||||
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.118.tgz#36fb108d63194fe9b06078a84576e32af54d06b8"
|
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.121.tgz#a8d37f300228edfcbb6b4fffb343f0773dcfd933"
|
||||||
integrity sha512-t29fYu22jfPoi/pFAbPX4//bh5XEgqbECHttjsOsPboC8BsnyYUtQSA/pY6PP/M/hMAW74QslnTK6iU/1OuSaQ==
|
integrity sha512-gvtKKZECPD2MU5X4MwPUKr2hSOs1+s1DA9laP3aPnmH0ukJRSFEhDOyjCMfH9k6ZdAe/vZCIbT4XucxLq/fOEA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@headlessui/vue" "^1.7.14"
|
"@headlessui/vue" "^1.7.14"
|
||||||
"@popperjs/core" "^2.11.2"
|
"@popperjs/core" "^2.11.2"
|
||||||
@ -4357,7 +4357,7 @@ svg-tags@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
|
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
|
||||||
integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==
|
integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==
|
||||||
|
|
||||||
tailwindcss@^3.3.3:
|
tailwindcss@^3.4.15:
|
||||||
version "3.4.17"
|
version "3.4.17"
|
||||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63"
|
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63"
|
||||||
integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==
|
integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user