Merge pull request #835 from frappe/revert-833-invite-member-fix-2

This commit is contained in:
Shariq Ansari 2025-05-20 13:09:32 +05:30 committed by GitHub
commit e85ef93480
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 454 additions and 316 deletions

View File

@ -123,12 +123,6 @@ def invite_by_email(emails: str, role: str):
for email in to_invite: for email in to_invite:
frappe.get_doc(doctype="CRM Invitation", email=email, role=role).insert(ignore_permissions=True) frappe.get_doc(doctype="CRM Invitation", email=email, role=role).insert(ignore_permissions=True)
return {
"existing_members": existing_members,
"existing_invites": existing_invites,
"to_invite": to_invite,
}
@frappe.whitelist() @frappe.whitelist()
def get_file_uploader_defaults(doctype: str): def get_file_uploader_defaults(doctype: str):

View File

@ -65,7 +65,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-05-19 17:57:24.610295", "modified": "2024-09-16 19:40:19.340948",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Form Script", "name": "CRM Form Script",
@ -83,19 +83,9 @@
"role": "Sales Manager", "role": "Sales Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1
} }
], ],
"row_format": "Dynamic",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@ -21,7 +21,7 @@ class CRMInvitation(Document):
if frappe.local.dev_server: if frappe.local.dev_server:
print(f"Invite link for {self.email}: {invite_link}") print(f"Invite link for {self.email}: {invite_link}")
title = "Frappe CRM" title = f"Frappe CRM"
template = "crm_invitation" template = "crm_invitation"
frappe.sendmail( frappe.sendmail(
@ -44,24 +44,12 @@ class CRMInvitation(Document):
user = self.create_user_if_not_exists() user = self.create_user_if_not_exists()
user.append_roles(self.role) user.append_roles(self.role)
if self.role == "Sales User":
self.update_module_in_user(user, "FCRM")
user.save(ignore_permissions=True) user.save(ignore_permissions=True)
self.status = "Accepted" self.status = "Accepted"
self.accepted_at = frappe.utils.now() self.accepted_at = frappe.utils.now()
self.save(ignore_permissions=True) self.save(ignore_permissions=True)
def update_module_in_user(self, user, module):
block_modules = frappe.get_all(
"Module Def",
fields=["name as module"],
filters={"name": ["!=", module]},
)
if block_modules:
user.set("block_modules", block_modules)
def create_user_if_not_exists(self): def create_user_if_not_exists(self):
if not frappe.db.exists("User", self.email): if not frappe.db.exists("User", self.email):
first_name = self.email.split("@")[0].title() first_name = self.email.split("@")[0].title()

View File

@ -264,7 +264,7 @@ def create_customer_in_remote_site(customer, erpnext_crm_settings):
@frappe.whitelist() @frappe.whitelist()
def get_crm_form_script(): def get_crm_form_script():
return """ return """
async function setupForm({ doc, call, $dialog, updateField, toast }) { async function setupForm({ doc, call, $dialog, updateField, createToast }) {
let actions = []; let actions = [];
let is_erpnext_integration_enabled = await call("frappe.client.get_single_value", {doctype: "ERPNext CRM Settings", field: "enabled"}); let is_erpnext_integration_enabled = await call("frappe.client.get_single_value", {doctype: "ERPNext CRM Settings", field: "enabled"});
if (!["Lost", "Won"].includes(doc?.status) && is_erpnext_integration_enabled) { if (!["Lost", "Won"].includes(doc?.status) && is_erpnext_integration_enabled) {

@ -1 +1 @@
Subproject commit 8b615c0e899d75b99c7d36ec6df97b5d0386b2ca Subproject commit 29307e4fffaacdbb3d9c5d95c5270b2f245a5607

View File

@ -150,8 +150,6 @@ declare module 'vue' {
ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default'] ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default']
ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default'] ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default']
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default'] LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
LucidePlus: typeof import('~icons/lucide/plus')['default']
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default'] MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default'] MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default'] MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default']
@ -175,7 +173,6 @@ declare module 'vue' {
OrganizationsIcon: typeof import('./src/components/Icons/OrganizationsIcon.vue')['default'] OrganizationsIcon: typeof import('./src/components/Icons/OrganizationsIcon.vue')['default']
OrganizationsListView: typeof import('./src/components/ListViews/OrganizationsListView.vue')['default'] OrganizationsListView: typeof import('./src/components/ListViews/OrganizationsListView.vue')['default']
OutboundCallIcon: typeof import('./src/components/Icons/OutboundCallIcon.vue')['default'] OutboundCallIcon: typeof import('./src/components/Icons/OutboundCallIcon.vue')['default']
Password: typeof import('./src/components/Controls/Password.vue')['default']
PauseIcon: typeof import('./src/components/Icons/PauseIcon.vue')['default'] PauseIcon: typeof import('./src/components/Icons/PauseIcon.vue')['default']
PhoneIcon: typeof import('./src/components/Icons/PhoneIcon.vue')['default'] PhoneIcon: typeof import('./src/components/Icons/PhoneIcon.vue')['default']
PinIcon: typeof import('./src/components/Icons/PinIcon.vue')['default'] PinIcon: typeof import('./src/components/Icons/PinIcon.vue')['default']
@ -210,7 +207,6 @@ declare module 'vue' {
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']
SortIcon: typeof import('./src/components/Icons/SortIcon.vue')['default'] SortIcon: typeof import('./src/components/Icons/SortIcon.vue')['default']
SquareAsterisk: typeof import('./src/components/Icons/SquareAsterisk.vue')['default']
StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default'] StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default']
SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default'] SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default']
TableMultiselectInput: typeof import('./src/components/Controls/TableMultiselectInput.vue')['default'] TableMultiselectInput: typeof import('./src/components/Controls/TableMultiselectInput.vue')['default']

View File

@ -216,13 +216,6 @@
:options="field.options" :options="field.options"
@change="(e) => fieldChange(e.target.value, field, row)" @change="(e) => fieldChange(e.target.value, field, row)"
/> />
<Password
v-else-if="field.fieldtype === 'Password'"
variant="outline"
:value="row[field.fieldname]"
:disabled="Boolean(field.read_only)"
@change="fieldChange($event.target.value, field, row)"
/>
<FormattedInput <FormattedInput
v-else-if="field.fieldtype === 'Int'" v-else-if="field.fieldtype === 'Int'"
class="[&_input]:text-right" class="[&_input]:text-right"
@ -332,7 +325,6 @@
</template> </template>
<script setup> <script setup>
import Password from '@/components/Controls/Password.vue'
import FormattedInput from '@/components/Controls/FormattedInput.vue' import FormattedInput from '@/components/Controls/FormattedInput.vue'
import GridFieldsEditorModal from '@/components/Controls/GridFieldsEditorModal.vue' import GridFieldsEditorModal from '@/components/Controls/GridFieldsEditorModal.vue'
import GridRowFieldsModal from '@/components/Controls/GridRowFieldsModal.vue' import GridRowFieldsModal from '@/components/Controls/GridRowFieldsModal.vue'

View File

@ -58,21 +58,6 @@
class="p-1.5 max-h-[12rem] overflow-y-auto" class="p-1.5 max-h-[12rem] overflow-y-auto"
static static
> >
<div
v-if="!options.length"
class="flex gap-2 rounded px-2 py-1 text-base text-ink-gray-5"
>
<FeatherIcon
v-if="fetchContacts"
name="search"
class="h-4"
/>
{{
fetchContacts
? __('No results found')
: __('Type an email address to add')
}}
</div>
<ComboboxOption <ComboboxOption
v-for="option in options" v-for="option in options"
:key="option.value" :key="option.value"
@ -152,10 +137,6 @@ const props = defineProps({
type: Function, type: Function,
default: (value) => `${value} is an Invalid value`, default: (value) => `${value} is an Invalid value`,
}, },
fetchContacts: {
type: Boolean,
default: true,
},
}) })
const values = defineModel() const values = defineModel()
@ -210,19 +191,17 @@ const filterOptions = createResource({
}) })
const options = computed(() => { const options = computed(() => {
let searchedContacts = props.fetchContacts ? filterOptions.data : [] let searchedContacts = filterOptions.data || []
if (!searchedContacts?.length && query.value) { if (!searchedContacts.length && query.value) {
searchedContacts.push({ searchedContacts.push({
label: query.value, label: query.value,
value: query.value, value: query.value,
}) })
} }
return searchedContacts || [] return searchedContacts
}) })
function reload(val) { function reload(val) {
if (!props.fetchContacts) return
filterOptions.update({ filterOptions.update({
params: { txt: val }, params: { txt: val },
}) })

View File

@ -1,35 +0,0 @@
<template>
<FormControl
:type="show ? 'text' : 'password'"
:value="modelValue || value"
v-bind="$attrs"
>
<template #suffix>
<Button v-show="showEye" class="!h-4" @click="show = !show">
<FeatherIcon :name="show ? 'eye-off' : 'eye'" class="h-3" />
</Button>
</template>
</FormControl>
</template>
<script setup>
import { FormControl } from 'frappe-ui'
import { ref, computed } from 'vue'
const props = defineProps({
modelValue: {
type: [String, Number],
default: '',
},
value: {
type: [String, Number],
default: '',
},
})
const show = ref(false)
const showEye = computed(() => {
let v = props.modelValue || props.value
return !v?.includes('*')
})
</script>

View File

@ -136,6 +136,7 @@
<DateTimePicker <DateTimePicker
v-else-if="field.fieldtype === 'Datetime'" v-else-if="field.fieldtype === 'Datetime'"
:value="data[field.fieldname]" :value="data[field.fieldname]"
icon-left=""
:formatter="(date) => getFormat(date, '', true, true)" :formatter="(date) => getFormat(date, '', true, true)"
:placeholder="getPlaceholder(field)" :placeholder="getPlaceholder(field)"
input-class="border-none" input-class="border-none"
@ -143,6 +144,7 @@
/> />
<DatePicker <DatePicker
v-else-if="field.fieldtype === 'Date'" v-else-if="field.fieldtype === 'Date'"
icon-left=""
:value="data[field.fieldname]" :value="data[field.fieldname]"
:formatter="(date) => getFormat(date, '', true)" :formatter="(date) => getFormat(date, '', true)"
:placeholder="getPlaceholder(field)" :placeholder="getPlaceholder(field)"
@ -159,18 +161,11 @@
:description="field.description" :description="field.description"
@change="fieldChange($event.target.value, field)" @change="fieldChange($event.target.value, field)"
/> />
<Password
v-else-if="field.fieldtype === 'Password'"
:value="data[field.fieldname]"
:placeholder="getPlaceholder(field)"
:description="field.description"
@change="fieldChange($event.target.value, field)"
/>
<FormattedInput <FormattedInput
v-else-if="field.fieldtype === 'Int'" v-else-if="['Int'].includes(field.fieldtype)"
type="text" type="number"
:placeholder="getPlaceholder(field)" :placeholder="getPlaceholder(field)"
:value="data[field.fieldname] || '0'" :value="data[field.fieldname]"
:disabled="Boolean(field.read_only)" :disabled="Boolean(field.read_only)"
:description="field.description" :description="field.description"
@change="fieldChange($event.target.value, field)" @change="fieldChange($event.target.value, field)"
@ -214,7 +209,6 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import Password from '@/components/Controls/Password.vue'
import FormattedInput from '@/components/Controls/FormattedInput.vue' import FormattedInput from '@/components/Controls/FormattedInput.vue'
import EditIcon from '@/components/Icons/EditIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'

View File

@ -104,7 +104,7 @@
import FilesUploaderArea from '@/components/FilesUploader/FilesUploaderArea.vue' import FilesUploaderArea from '@/components/FilesUploader/FilesUploaderArea.vue'
import FilesUploadHandler from './filesUploaderHandler' import FilesUploadHandler from './filesUploaderHandler'
import { isMobileView } from '@/composables/settings' import { isMobileView } from '@/composables/settings'
import { toast } from 'frappe-ui' import { createToast } from '@/utils'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
const props = defineProps({ const props = defineProps({
@ -165,7 +165,12 @@ function attachFiles() {
function uploadViaWebLink() { function uploadViaWebLink() {
let fileUrl = filesUploaderArea.value.webLink let fileUrl = filesUploaderArea.value.webLink
if (!fileUrl) { if (!fileUrl) {
toast.error(__('Please enter a valid URL')) createToast({
title: __('Error'),
title: __('Please enter a valid URL'),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
return return
} }
fileUrl = decodeURI(fileUrl) fileUrl = decodeURI(fileUrl)

View File

@ -126,13 +126,8 @@
import FileTextIcon from '@/components/Icons/FileTextIcon.vue' import FileTextIcon from '@/components/Icons/FileTextIcon.vue'
import FileAudioIcon from '@/components/Icons/FileAudioIcon.vue' import FileAudioIcon from '@/components/Icons/FileAudioIcon.vue'
import FileVideoIcon from '@/components/Icons/FileVideoIcon.vue' import FileVideoIcon from '@/components/Icons/FileVideoIcon.vue'
import { formatDate, convertSize } from '@/utils' import { createToast, formatDate, convertSize } from '@/utils'
import { import { FormControl, CircularProgressBar, createResource } from 'frappe-ui'
FormControl,
CircularProgressBar,
createResource,
toast,
} from 'frappe-ui'
import { ref, onMounted, watch, onUnmounted } from 'vue' import { ref, onMounted, watch, onUnmounted } from 'vue'
const props = defineProps({ const props = defineProps({
@ -329,18 +324,24 @@ function checkRestrictions(file) {
if (!isCorrectType) { if (!isCorrectType) {
console.warn('File skipped because of invalid file type', file) console.warn('File skipped because of invalid file type', file)
toast.warning( createToast({
__('File "{0}" was skipped because of invalid file type', [file.name]), title: __('File "{0}" was skipped because of invalid file type', [
) file.name,
]),
icon: 'alert-circle',
iconClasses: 'text-orange-600',
})
} }
if (!validFileSize) { if (!validFileSize) {
console.warn('File skipped because of invalid file size', file.size, file) console.warn('File skipped because of invalid file size', file.size, file)
toast.warning( createToast({
__('File "{0}" was skipped because size exceeds {1} MB', [ title: __('File "{0}" was skipped because size exceeds {1} MB', [
file.name, file.name,
maxFileSize / (1024 * 1024), maxFileSize / (1024 * 1024),
]), ]),
) icon: 'alert-circle',
iconClasses: 'text-orange-600',
})
} }
return isCorrectType && validFileSize return isCorrectType && validFileSize
@ -362,7 +363,11 @@ function showMaxFilesNumberWarning(file, maxNumberOfFiles) {
) )
} }
toast.warning(message) createToast({
title: message,
icon: 'alert-circle',
iconClasses: 'text-orange-600',
})
} }
function removeFile(name) { function removeFile(name) {

View File

@ -1,19 +0,0 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-square-asterisk-icon lucide-square-asterisk"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M12 8v8" />
<path d="m8.5 14 7-4" />
<path d="m8.5 10 7 4" />
</svg>
</template>

View File

@ -150,7 +150,6 @@ import Section from '@/components/Section.vue'
import Email2Icon from '@/components/Icons/Email2Icon.vue' import Email2Icon from '@/components/Icons/Email2Icon.vue'
import PinIcon from '@/components/Icons/PinIcon.vue' import PinIcon from '@/components/Icons/PinIcon.vue'
import UserDropdown from '@/components/UserDropdown.vue' import UserDropdown from '@/components/UserDropdown.vue'
import SquareAsterisk from '@/components/Icons/SquareAsterisk.vue'
import LeadsIcon from '@/components/Icons/LeadsIcon.vue' import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue' import DealsIcon from '@/components/Icons/DealsIcon.vue'
import ContactsIcon from '@/components/Icons/ContactsIcon.vue' import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
@ -322,17 +321,6 @@ const showIntermediateModal = ref(false)
const currentStep = ref({}) const currentStep = ref({})
const steps = reactive([ const steps = reactive([
{
name: 'setup_your_password',
title: __('Setup your password'),
icon: markRaw(SquareAsterisk),
completed: false,
onClick: () => {
minimize.value = true
showSettings.value = true
activeSettingsPage.value = 'Profile'
},
},
{ {
name: 'create_first_lead', name: 'create_first_lead',
title: __('Create your first lead'), title: __('Create your first lead'),

View File

@ -19,10 +19,10 @@
<script setup> <script setup>
import EditValueModal from '@/components/Modals/EditValueModal.vue' import EditValueModal from '@/components/Modals/EditValueModal.vue'
import AssignmentModal from '@/components/Modals/AssignmentModal.vue' import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
import { setupListCustomizations } from '@/utils' import { setupListCustomizations, createToast } from '@/utils'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { call, toast } from 'frappe-ui' import { call } from 'frappe-ui'
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -75,7 +75,11 @@ function convertToDeal(selections, unselectAll) {
call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', { call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
lead: name, lead: name,
}).then(() => { }).then(() => {
toast.success(__('Converted successfully')) createToast({
title: __('Converted successfully'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
list.value.reload() list.value.reload()
unselectAll() unselectAll()
close() close()
@ -106,7 +110,11 @@ function deleteValues(selections, unselectAll) {
items: JSON.stringify(Array.from(selections)), items: JSON.stringify(Array.from(selections)),
doctype: props.doctype, doctype: props.doctype,
}).then(() => { }).then(() => {
toast.success(__('Deleted successfully')) createToast({
title: __('Deleted successfully'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
unselectAll() unselectAll()
list.value.reload() list.value.reload()
close() close()
@ -146,7 +154,11 @@ function clearAssignemnts(selections, unselectAll) {
names: JSON.stringify(Array.from(selections)), names: JSON.stringify(Array.from(selections)),
ignore_permissions: true, ignore_permissions: true,
}).then(() => { }).then(() => {
toast.success(__('Assignment cleared successfully')) createToast({
title: __('Assignment cleared successfully'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
reload(unselectAll) reload(unselectAll)
close() close()
}) })
@ -203,8 +215,7 @@ function bulkActions(selections, unselectAll) {
selections, selections,
unselectAll, unselectAll,
call, call,
createToast: toast.create, createToast,
toast,
$dialog, $dialog,
router, router,
}), }),
@ -224,8 +235,7 @@ onMounted(async () => {
let customization = await setupListCustomizations(list.value.data, { let customization = await setupListCustomizations(list.value.data, {
list: list.value, list: list.value,
call, call,
createToast: toast.create, createToast,
toast,
$dialog, $dialog,
$socket, $socket,
router, router,

View File

@ -93,8 +93,9 @@
<script setup> <script setup>
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { createResource, toast } from 'frappe-ui' import { createResource } from 'frappe-ui'
import CircleAlert from '~icons/lucide/circle-alert' import CircleAlert from '~icons/lucide/circle-alert'
import { createToast } from '@/utils'
import { import {
customProviderFields, customProviderFields,
popularProviderFields, popularProviderFields,
@ -138,7 +139,11 @@ const addEmailRes = createResource({
} }
}, },
onSuccess: () => { onSuccess: () => {
toast.success(__('Email account created successfully')) createToast({
title: __('Email account created successfully'),
icon: 'check',
iconClasses: 'text-green-600',
})
emit('update:step', 'email-list') emit('update:step', 'email-list')
}, },
onError: () => { onError: () => {

View File

@ -82,7 +82,7 @@
<script setup> <script setup>
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { call, toast } from 'frappe-ui' import { call } from 'frappe-ui'
import EmailProviderIcon from './EmailProviderIcon.vue' import EmailProviderIcon from './EmailProviderIcon.vue'
import { import {
emailIcon, emailIcon,
@ -92,6 +92,7 @@ import {
validateInputs, validateInputs,
incomingOutgoingFields, incomingOutgoingFields,
} from './emailConfig' } from './emailConfig'
import { createToast } from '@/utils'
import CircleAlert from '~icons/lucide/circle-alert' import CircleAlert from '~icons/lucide/circle-alert'
const props = defineProps({ const props = defineProps({
@ -147,7 +148,11 @@ async function updateAccount() {
const values = updatedEmailAccount const values = updatedEmailAccount
if (!nameChanged && !otherFieldsChanged) { if (!nameChanged && !otherFieldsChanged) {
toast.info(__('No changes made')) createToast({
title: __('No changes made'),
icon: 'info',
iconClasses: 'text-blue-600',
})
return return
} }
@ -205,7 +210,11 @@ async function callSetValue(values) {
function succesHandler() { function succesHandler() {
emit('update:step', 'email-list') emit('update:step', 'email-list')
toast.success(__('Email account updated successfully')) createToast({
title: __('Email account updated successfully'),
icon: 'check',
iconClasses: 'text-green-600',
})
} }
function errorHandler() { function errorHandler() {

View File

@ -20,7 +20,6 @@
:error-message=" :error-message="
(value) => __('{0} is an invalid email address', [value]) (value) => __('{0} is an invalid email address', [value])
" "
:fetchContacts="false"
/> />
</div> </div>
<FormControl <FormControl
@ -128,17 +127,10 @@ const inviteByEmail = createResource({
role: role.value, role: role.value,
} }
}, },
onSuccess(data) { onSuccess() {
if (data?.existing_invites?.length) {
error.value = __('Agent with email {0} already exists', [
data.existing_invites.join(', '),
])
} else {
role.value = 'Sales User'
error.value = null
}
invitees.value = [] invitees.value = []
role.value = 'Sales User'
error.value = null
pendingInvitations.reload() pendingInvitations.reload()
updateOnboardingStep('invite_your_team') updateOnboardingStep('invite_your_team')
}, },

View File

@ -57,7 +57,7 @@
v-model="profile.email" v-model="profile.email"
:disabled="true" :disabled="true"
/> />
<Password <FormControl
class="w-full" class="w-full"
label="Set new password" label="Set new password"
v-model="profile.new_password" v-model="profile.new_password"
@ -77,15 +77,13 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import Password from '@/components/Controls/Password.vue'
import ProfileImageEditor from '@/components/Settings/ProfileImageEditor.vue' import ProfileImageEditor from '@/components/Settings/ProfileImageEditor.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { Dialog, Avatar, createResource, ErrorMessage, toast } from 'frappe-ui' import { createToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe' import { Dialog, Avatar, createResource, ErrorMessage } from 'frappe-ui'
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
const { getUser, users } = usersStore() const { getUser, users } = usersStore()
const { updateOnboardingStep } = useOnboarding('frappecrm')
const user = computed(() => getUser() || {}) const user = computed(() => getUser() || {})
@ -97,13 +95,6 @@ const error = ref('')
function updateUser() { function updateUser() {
loading.value = true loading.value = true
let passwordUpdated = false
if (profile.value.new_password) {
passwordUpdated = true
}
const fieldname = { const fieldname = {
first_name: profile.value.first_name, first_name: profile.value.first_name,
last_name: profile.value.last_name, last_name: profile.value.last_name,
@ -120,14 +111,15 @@ function updateUser() {
}, },
auto: true, auto: true,
onSuccess: () => { onSuccess: () => {
if (passwordUpdated) {
updateOnboardingStep('setup_your_password')
}
loading.value = false loading.value = false
error.value = '' error.value = ''
profile.value.new_password = '' profile.value.new_password = ''
showEditProfilePhotoModal.value = false showEditProfilePhotoModal.value = false
toast.success(__('Profile updated successfully')) createToast({
title: __('Profile updated successfully'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
users.reload() users.reload()
}, },
onError: (err) => { onError: (err) => {

View File

@ -42,10 +42,9 @@ import {
createResource, createResource,
Spinner, Spinner,
Badge, Badge,
toast,
ErrorMessage, ErrorMessage,
} from 'frappe-ui' } from 'frappe-ui'
import { getRandom } from '@/utils' import { createToast, getRandom } from '@/utils'
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps({ const props = defineProps({
@ -80,10 +79,20 @@ const data = createDocumentResource({
auto: true, auto: true,
setValue: { setValue: {
onSuccess: () => { onSuccess: () => {
toast.success(__(props.successMessage)) createToast({
title: __('Success'),
text: __(props.successMessage),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
}, },
onError: (err) => { onError: (err) => {
toast.error(err.message + ': ' + err.messages[0]) createToast({
title: __('Error'),
text: err.message + ': ' + err.messages[0],
icon: 'x',
iconClasses: 'text-ink-red-4',
})
}, },
}, },
}) })

View File

@ -87,8 +87,7 @@ import {
} from 'frappe-ui' } from 'frappe-ui'
import { defaultCallingMedium } from '@/composables/settings' import { defaultCallingMedium } from '@/composables/settings'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { toast } from 'frappe-ui' import { createToast, getRandom } from '@/utils'
import { getRandom } from '@/utils'
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
const { isManager, isAgent } = usersStore() const { isManager, isAgent } = usersStore()
@ -120,10 +119,20 @@ const twilio = createDocumentResource({
auto: true, auto: true,
setValue: { setValue: {
onSuccess: () => { onSuccess: () => {
toast.success(__('Twilio settings updated successfully')) createToast({
title: __('Success'),
text: __('Twilio settings updated successfully'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
}, },
onError: (err) => { onError: (err) => {
toast.error(err.message + ': ' + err.messages[0]) createToast({
title: __('Error'),
text: err.message + ': ' + err.messages[0],
icon: 'x',
iconClasses: 'text-ink-red-4',
})
}, },
}, },
}) })
@ -135,10 +144,20 @@ const exotel = createDocumentResource({
auto: true, auto: true,
setValue: { setValue: {
onSuccess: () => { onSuccess: () => {
toast.success(__('Exotel settings updated successfully')) createToast({
title: __('Success'),
text: __('Exotel settings updated successfully'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
}, },
onError: (err) => { onError: (err) => {
toast.error(err.message + ': ' + err.messages[0]) createToast({
title: __('Error'),
text: err.message + ': ' + err.messages[0],
icon: 'x',
iconClasses: 'text-ink-red-4',
})
}, },
}, },
}) })
@ -275,7 +294,12 @@ async function updateMedium() {
}) })
mediumChanged.value = false mediumChanged.value = false
error.value = '' error.value = ''
toast.success(__('Default calling medium updated successfully')) createToast({
title: __('Success'),
text: __('Default calling medium updated successfully'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
const error = ref('') const error = ref('')

View File

@ -275,20 +275,11 @@
" "
:disabled="Boolean(field.read_only)" :disabled="Boolean(field.read_only)"
/> />
<Password
v-else-if="field.fieldtype === 'Password'"
class="form-control"
:value="document.doc[field.fieldname]"
:placeholder="field.placeholder"
:debounce="500"
@change.stop="fieldChange($event.target.value, field)"
:disabled="Boolean(field.read_only)"
/>
<FormattedInput <FormattedInput
v-else-if="field.fieldtype === 'Int'" v-else-if="field.fieldtype === 'Int'"
class="form-control" class="form-control"
type="text" type="text"
:value="document.doc[field.fieldname] || '0'" v-model="document.doc[field.fieldname]"
:placeholder="field.placeholder" :placeholder="field.placeholder"
:debounce="500" :debounce="500"
@change.stop="fieldChange($event.target.value, field)" @change.stop="fieldChange($event.target.value, field)"
@ -375,7 +366,6 @@
</template> </template>
<script setup> <script setup>
import Password from '@/components/Controls/Password.vue'
import FormattedInput from '@/components/Controls/FormattedInput.vue' import FormattedInput from '@/components/Controls/FormattedInput.vue'
import Section from '@/components/Section.vue' import Section from '@/components/Section.vue'
import NestedPopover from '@/components/NestedPopover.vue' import NestedPopover from '@/components/NestedPopover.vue'

View File

@ -35,9 +35,7 @@
/> />
<div v-if="isDefaultMedium" class="text-sm text-ink-gray-4"> <div v-if="isDefaultMedium" class="text-sm text-ink-gray-4">
{{ {{ __('You can change the default calling medium from the settings') }}
__('You can change the default calling medium from the settings')
}}
</div> </div>
</div> </div>
</div> </div>
@ -53,7 +51,8 @@ import {
defaultCallingMedium, defaultCallingMedium,
} from '@/composables/settings' } from '@/composables/settings'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { FormControl, call, toast } from 'frappe-ui' import { createToast } from '@/utils'
import { FormControl, call } from 'frappe-ui'
import { nextTick, ref, watch } from 'vue' import { nextTick, ref, watch } from 'vue'
const { setMakeCall } = globalStore() const { setMakeCall } = globalStore()
@ -108,9 +107,13 @@ async function setDefaultCallingMedium() {
}) })
defaultCallingMedium.value = callMedium.value defaultCallingMedium.value = callMedium.value
toast.success( createToast({
__('Default calling medium set successfully to {0}', [callMedium.value]), title: __('Default calling medium set successfully to {0}', [
) callMedium.value,
]),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
watch( watch(

View File

@ -244,10 +244,11 @@ import NoteIcon from '@/components/Icons/NoteIcon.vue'
import TaskIcon from '@/components/Icons/TaskIcon.vue' import TaskIcon from '@/components/Icons/TaskIcon.vue'
import TaskPanel from '@/components/Telephony/TaskPanel.vue' import TaskPanel from '@/components/Telephony/TaskPanel.vue'
import CountUpTimer from '@/components/CountUpTimer.vue' import CountUpTimer from '@/components/CountUpTimer.vue'
import { createToast } from '@/utils'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { useDraggable, useWindowSize } from '@vueuse/core' import { useDraggable, useWindowSize } from '@vueuse/core'
import { TextEditor, Avatar, Button, createResource, toast } from 'frappe-ui' import { TextEditor, Avatar, Button, createResource } from 'frappe-ui'
import { ref, onBeforeUnmount, watch, nextTick } from 'vue' import { ref, onBeforeUnmount, watch, nextTick } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -412,7 +413,12 @@ function makeOutgoingCall(number) {
showSmallCallPopup.value = false showSmallCallPopup.value = false
}, },
onError(err) { onError(err) {
toast.error(err.messages[0]) createToast({
title: 'Error',
text: err.messages[0],
icon: 'x',
iconClasses: 'text-red-600',
})
}, },
}) })
} }

View File

@ -312,12 +312,11 @@ import { globalStore } from '@/stores/global'
import { viewsStore } from '@/stores/views' import { viewsStore } from '@/stores/views'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
import { isEmoji } from '@/utils' import { isEmoji, createToast } from '@/utils'
import { import {
Tooltip, Tooltip,
createResource, createResource,
Dropdown, Dropdown,
toast,
call, call,
FeatherIcon, FeatherIcon,
usePageMeta, usePageMeta,
@ -728,7 +727,12 @@ const updateQuickFilters = createResource({
quickFilters.update({ params: { doctype: props.doctype, cached: false } }) quickFilters.update({ params: { doctype: props.doctype, cached: false } })
quickFilters.reload() quickFilters.reload()
toast.success(__('Quick Filters updated successfully'))
createToast({
title: __('Quick Filters updated successfully'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
}, },
}) })

View File

@ -1,6 +1,6 @@
import { getScript } from '@/data/script' import { getScript } from '@/data/script'
import { runSequentially } from '@/utils' import { createToast, runSequentially } from '@/utils'
import { createDocumentResource, toast } from 'frappe-ui' import { createDocumentResource } from 'frappe-ui'
const documentsCache = {} const documentsCache = {}
const controllersCache = {} const controllersCache = {}
@ -17,11 +17,19 @@ export function useDocument(doctype, docname) {
onSuccess: () => setupFormScript(), onSuccess: () => setupFormScript(),
setValue: { setValue: {
onSuccess: () => { onSuccess: () => {
toast.success(__('Document updated successfully')) createToast({
title: __('Document updated successfully'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
}, },
onError: (err) => { onError: (err) => {
toast.error(__('Error updating document')) createToast({
console.error(err) title: __('Error updating document'),
text: err.messages[0],
icon: 'x',
iconClasses: 'text-red-600',
})
}, },
}, },
}) })

View File

@ -1,6 +1,7 @@
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
import { call, createListResource, toast } from 'frappe-ui' import { createToast } from '@/utils'
import { call, createListResource } from 'frappe-ui'
import { reactive } from 'vue' import { reactive } from 'vue'
import router from '@/router' import router from '@/router'
@ -33,7 +34,7 @@ export function getScript(doctype, view = 'Form') {
const { $dialog, $socket, makeCall } = globalStore() const { $dialog, $socket, makeCall } = globalStore()
helpers.createDialog = $dialog helpers.createDialog = $dialog
helpers.toast = toast helpers.createToast = createToast
helpers.socket = $socket helpers.socket = $socket
helpers.router = router helpers.router = router
helpers.call = call helpers.call = call

View File

@ -186,7 +186,7 @@ import CameraIcon from '@/components/Icons/CameraIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue' import DealsIcon from '@/components/Icons/DealsIcon.vue'
import DealsListView from '@/components/ListViews/DealsListView.vue' import DealsListView from '@/components/ListViews/DealsListView.vue'
import AddressModal from '@/components/Modals/AddressModal.vue' import AddressModal from '@/components/Modals/AddressModal.vue'
import { formatDate, timeAgo } from '@/utils' import { formatDate, timeAgo, createToast } from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings' import { getSettings } from '@/stores/settings'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
@ -204,7 +204,6 @@ import {
createResource, createResource,
usePageMeta, usePageMeta,
Dropdown, Dropdown,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, computed, h } from 'vue' import { ref, computed, h } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
@ -523,7 +522,11 @@ async function setAsPrimary(field, value) {
}) })
if (d) { if (d) {
contact.reload() contact.reload()
toast.success(__('Contact updated')) createToast({
title: 'Contact updated',
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }
@ -536,7 +539,11 @@ async function createNew(field, value) {
}) })
if (d) { if (d) {
contact.reload() contact.reload()
toast.success(__('Contact updated')) createToast({
title: 'Contact updated',
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }
@ -549,7 +556,11 @@ async function editOption(doctype, name, fieldname, value) {
}) })
if (d) { if (d) {
contact.reload() contact.reload()
toast.success(__('Contact updated')) createToast({
title: 'Contact updated',
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }
@ -559,7 +570,11 @@ async function deleteOption(doctype, name) {
name, name,
}) })
await contact.reload() await contact.reload()
toast.success(__('Contact updated')) createToast({
title: 'Contact updated',
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
async function updateField(fieldname, value) { async function updateField(fieldname, value) {
@ -569,7 +584,11 @@ async function updateField(fieldname, value) {
fieldname, fieldname,
value, value,
}) })
toast.success(__('Contact updated')) createToast({
title: 'Contact updated',
icon: 'check',
iconClasses: 'text-ink-green-3',
})
contact.reload() contact.reload()
} }

View File

@ -89,7 +89,7 @@
@click=" @click="
deal.data.email deal.data.email
? openEmailBox() ? openEmailBox()
: toast.error(__('No email set')) : _errorMessage(__('No email set'))
" "
/> />
</Button> </Button>
@ -103,7 +103,7 @@
@click=" @click="
deal.data.website deal.data.website
? openWebsite(deal.data.website) ? openWebsite(deal.data.website)
: toast.error(__('No website set')) : _errorMessage(__('No website set'))
" "
/> />
</Button> </Button>
@ -332,8 +332,10 @@ import SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue' import CustomActions from '@/components/CustomActions.vue'
import { import {
openWebsite, openWebsite,
createToast,
setupAssignees, setupAssignees,
setupCustomizations, setupCustomizations,
errorMessage as _errorMessage,
copyToClipboard, copyToClipboard,
} from '@/utils' } from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
@ -351,7 +353,6 @@ import {
Breadcrumbs, Breadcrumbs,
call, call,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import { ref, computed, h, onMounted, onBeforeUnmount } from 'vue' import { ref, computed, h, onMounted, onBeforeUnmount } from 'vue'
@ -400,9 +401,8 @@ const deal = createResource({
$dialog, $dialog,
$socket, $socket,
router, router,
toast,
updateField, updateField,
createToast: toast.create, createToast,
deleteDoc: deleteDeal, deleteDoc: deleteDeal,
resource: { resource: {
deal, deal,
@ -429,7 +429,11 @@ const organization = createResource({
onMounted(() => { onMounted(() => {
$socket.on('crm_customer_created', () => { $socket.on('crm_customer_created', () => {
toast.success(__('Customer created successfully')) createToast({
title: __('Customer created successfully'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
}) })
if (deal.data) { if (deal.data) {
@ -465,11 +469,20 @@ function updateDeal(fieldname, value, callback) {
onSuccess: () => { onSuccess: () => {
deal.reload() deal.reload()
reload.value = true reload.value = true
toast.success(__('Deal updated')) createToast({
title: __('Deal updated'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
callback?.() callback?.()
}, },
onError: (err) => { onError: (err) => {
toast.error(__('Error updating deal: {0}', [err.messages?.[0]])) createToast({
title: __('Error updating deal'),
text: __(err.messages?.[0]),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
}, },
}) })
} }
@ -477,7 +490,12 @@ function updateDeal(fieldname, value, callback) {
function validateRequired(fieldname, value) { function validateRequired(fieldname, value) {
let meta = deal.data.fields_meta || {} let meta = deal.data.fields_meta || {}
if (meta[fieldname]?.reqd && !value) { if (meta[fieldname]?.reqd && !value) {
toast.error(__('{0} is a required field', [meta[fieldname].label])) createToast({
title: __('Error Updating Deal'),
text: __('{0} is a required field', [meta[fieldname].label]),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
return true return true
} }
return false return false
@ -628,7 +646,11 @@ function contactOptions(contact) {
async function addContact(contact) { async function addContact(contact) {
if (dealContacts.data?.find((c) => c.name === contact)) { if (dealContacts.data?.find((c) => c.name === contact)) {
toast.error(__('Contact already added')) createToast({
title: __('Contact already added'),
icon: 'x',
iconClasses: 'text-ink-red-3',
})
return return
} }
@ -638,7 +660,11 @@ async function addContact(contact) {
}) })
if (d) { if (d) {
dealContacts.reload() dealContacts.reload()
toast.success(__('Contact added')) createToast({
title: __('Contact added'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }
@ -649,7 +675,11 @@ async function removeContact(contact) {
}) })
if (d) { if (d) {
dealContacts.reload() dealContacts.reload()
toast.success(__('Contact removed')) createToast({
title: __('Contact removed'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }
@ -660,7 +690,11 @@ async function setPrimaryContact(contact) {
}) })
if (d) { if (d) {
dealContacts.reload() dealContacts.reload()
toast.success(__('Primary contact set')) createToast({
title: __('Primary contact set'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }
@ -683,12 +717,12 @@ function triggerCall() {
let mobile_no = primaryContact.mobile_no || null let mobile_no = primaryContact.mobile_no || null
if (!primaryContact) { if (!primaryContact) {
toast.error(__('No primary contact set')) _errorMessage(__('No primary contact set'))
return return
} }
if (!mobile_no) { if (!mobile_no) {
toast.error(__('No mobile number set')) _errorMessage(__('No mobile number set'))
return return
} }

View File

@ -124,7 +124,7 @@
() => () =>
lead.data.mobile_no lead.data.mobile_no
? makeCall(lead.data.mobile_no) ? makeCall(lead.data.mobile_no)
: toast.error(__('No phone number set')) : _errorMessage(__('No phone number set'))
" "
> >
<PhoneIcon class="h-4 w-4" /> <PhoneIcon class="h-4 w-4" />
@ -139,7 +139,7 @@
@click=" @click="
lead.data.email lead.data.email
? openEmailBox() ? openEmailBox()
: toast.error(__('No email set')) : _errorMessage(__('No email set'))
" "
/> />
</Button> </Button>
@ -153,7 +153,7 @@
@click=" @click="
lead.data.website lead.data.website
? openWebsite(lead.data.website) ? openWebsite(lead.data.website)
: toast.error(__('No website set')) : _errorMessage(__('No website set'))
" "
/> />
</Button> </Button>
@ -344,8 +344,10 @@ import SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue' import CustomActions from '@/components/CustomActions.vue'
import { import {
openWebsite, openWebsite,
createToast,
setupAssignees, setupAssignees,
setupCustomizations, setupCustomizations,
errorMessage as _errorMessage,
copyToClipboard, copyToClipboard,
} from '@/utils' } from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
@ -372,7 +374,6 @@ import {
Breadcrumbs, Breadcrumbs,
call, call,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import { ref, reactive, computed, onMounted, watch } from 'vue' import { ref, reactive, computed, onMounted, watch } from 'vue'
@ -414,9 +415,8 @@ const lead = createResource({
$dialog, $dialog,
$socket, $socket,
router, router,
toast,
updateField, updateField,
createToast: toast.create, createToast,
deleteDoc: deleteLead, deleteDoc: deleteLead,
resource: { lead, sections }, resource: { lead, sections },
call, call,
@ -457,11 +457,20 @@ function updateLead(fieldname, value, callback) {
onSuccess: () => { onSuccess: () => {
lead.reload() lead.reload()
reload.value = true reload.value = true
toast.success(__('Lead updated successfully')) createToast({
title: __('Lead updated'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
callback?.() callback?.()
}, },
onError: (err) => { onError: (err) => {
toast.error(err.messages?.[0] || __('Error updating lead')) createToast({
title: __('Error updating lead'),
text: __(err.messages?.[0]),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
}, },
}) })
} }
@ -469,7 +478,12 @@ function updateLead(fieldname, value, callback) {
function validateRequired(fieldname, value) { function validateRequired(fieldname, value) {
let meta = lead.data.fields_meta || {} let meta = lead.data.fields_meta || {}
if (meta[fieldname]?.reqd && !value) { if (meta[fieldname]?.reqd && !value) {
toast.error(__('{0} is a required field', [meta[fieldname].label])) createToast({
title: __('Error Updating Lead'),
text: __('{0} is a required field', [meta[fieldname].label]),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
return true return true
} }
return false return false
@ -616,12 +630,22 @@ const existingOrganization = ref('')
async function convertToDeal() { async function convertToDeal() {
if (existingContactChecked.value && !existingContact.value) { if (existingContactChecked.value && !existingContact.value) {
toast.error(__('Please select an existing contact')) createToast({
title: __('Error'),
text: __('Please select an existing contact'),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
return return
} }
if (existingOrganizationChecked.value && !existingOrganization.value) { if (existingOrganizationChecked.value && !existingOrganization.value) {
toast.error(__('Please select an existing organization')) createToast({
title: __('Error'),
text: __('Please select an existing organization'),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
return return
} }
@ -639,7 +663,12 @@ async function convertToDeal() {
existing_contact: existingContact.value, existing_contact: existingContact.value,
existing_organization: existingOrganization.value, existing_organization: existingOrganization.value,
}).catch((err) => { }).catch((err) => {
toast.error(__('Error converting to deal: {0}', [err.messages?.[0]])) createToast({
title: __('Error converting to deal'),
text: __(err.messages?.[0]),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
}) })
if (_deal) { if (_deal) {
showConvertToDealModal.value = false showConvertToDealModal.value = false

View File

@ -169,7 +169,7 @@ import CameraIcon from '@/components/Icons/CameraIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue' import DealsIcon from '@/components/Icons/DealsIcon.vue'
import DealsListView from '@/components/ListViews/DealsListView.vue' import DealsListView from '@/components/ListViews/DealsListView.vue'
import AddressModal from '@/components/Modals/AddressModal.vue' import AddressModal from '@/components/Modals/AddressModal.vue'
import { formatDate, timeAgo } from '@/utils' import { formatDate, timeAgo, createToast } from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings' import { getSettings } from '@/stores/settings'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
@ -189,7 +189,6 @@ import {
createResource, createResource,
usePageMeta, usePageMeta,
Dropdown, Dropdown,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, computed, h } from 'vue' import { ref, computed, h } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
@ -497,7 +496,11 @@ async function setAsPrimary(field, value) {
}) })
if (d) { if (d) {
contact.reload() contact.reload()
toast.success(___('Contact updated')) createToast({
title: 'Contact updated',
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }
@ -510,7 +513,11 @@ async function createNew(field, value) {
}) })
if (d) { if (d) {
contact.reload() contact.reload()
toast.success(__('Contact updated')) createToast({
title: 'Contact updated',
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }
@ -523,7 +530,11 @@ async function editOption(doctype, name, fieldname, value) {
}) })
if (d) { if (d) {
contact.reload() contact.reload()
toast.success(__('Contact updated')) createToast({
title: 'Contact updated',
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }
@ -533,7 +544,11 @@ async function deleteOption(doctype, name) {
name, name,
}) })
await contact.reload() await contact.reload()
toast.success(__('Contact updated')) createToast({
title: 'Contact updated',
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
async function updateField(fieldname, value) { async function updateField(fieldname, value) {
@ -543,7 +558,11 @@ async function updateField(fieldname, value) {
fieldname, fieldname,
value, value,
}) })
toast.success(__('Contact updated')) createToast({
title: 'Contact updated',
icon: 'check',
iconClasses: 'text-ink-green-3',
})
contact.reload() contact.reload()
} }

View File

@ -256,7 +256,7 @@ import Link from '@/components/Controls/Link.vue'
import SidePanelLayout from '@/components/SidePanelLayout.vue' import SidePanelLayout from '@/components/SidePanelLayout.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 { setupAssignees, setupCustomizations } from '@/utils' import { createToast, setupAssignees, setupCustomizations } from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings' import { getSettings } from '@/stores/settings'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
@ -278,7 +278,6 @@ import {
Breadcrumbs, Breadcrumbs,
call, call,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, computed, h, onMounted } from 'vue' import { ref, computed, h, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
@ -315,9 +314,8 @@ const deal = createResource({
$dialog, $dialog,
$socket, $socket,
router, router,
toast,
updateField, updateField,
createToast: toast.create, createToast,
deleteDoc: deleteDeal, deleteDoc: deleteDeal,
resource: { resource: {
deal, deal,
@ -360,11 +358,20 @@ function updateDeal(fieldname, value, callback) {
onSuccess: () => { onSuccess: () => {
deal.reload() deal.reload()
reload.value = true reload.value = true
toast.success(__('Deal updated')) createToast({
title: __('Deal updated'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
callback?.() callback?.()
}, },
onError: (err) => { onError: (err) => {
toast.error(err.messages?.[0] || __('Error updating deal')) createToast({
title: __('Error updating deal'),
text: __(err.messages?.[0]),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
}, },
}) })
} }
@ -372,7 +379,12 @@ function updateDeal(fieldname, value, callback) {
function validateRequired(fieldname, value) { function validateRequired(fieldname, value) {
let meta = deal.data.fields_meta || {} let meta = deal.data.fields_meta || {}
if (meta[fieldname]?.reqd && !value) { if (meta[fieldname]?.reqd && !value) {
toast.error(__('{0} is a required field', [meta[fieldname].label])) createToast({
title: __('Error Updating Deal'),
text: __('{0} is a required field', [meta[fieldname].label]),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
return true return true
} }
return false return false
@ -529,7 +541,11 @@ function contactOptions(contact) {
async function addContact(contact) { async function addContact(contact) {
if (dealContacts.data?.find((c) => c.name === contact)) { if (dealContacts.data?.find((c) => c.name === contact)) {
toast.error(__('Contact already added')) createToast({
title: __('Contact already added'),
icon: 'x',
iconClasses: 'text-ink-red-3',
})
return return
} }
@ -539,7 +555,11 @@ async function addContact(contact) {
}) })
if (d) { if (d) {
dealContacts.reload() dealContacts.reload()
toast.success(__('Contact added')) createToast({
title: __('Contact added'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }
@ -550,7 +570,11 @@ async function removeContact(contact) {
}) })
if (d) { if (d) {
dealContacts.reload() dealContacts.reload()
toast.success(__('Contact removed')) createToast({
title: __('Contact removed'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }
@ -561,7 +585,11 @@ async function setPrimaryContact(contact) {
}) })
if (d) { if (d) {
dealContacts.reload() dealContacts.reload()
toast.success(__('Primary contact set')) createToast({
title: __('Primary contact set'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }

View File

@ -173,7 +173,7 @@ import Link from '@/components/Controls/Link.vue'
import SidePanelLayout from '@/components/SidePanelLayout.vue' import SidePanelLayout from '@/components/SidePanelLayout.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 { setupAssignees, setupCustomizations } from '@/utils' import { createToast, setupAssignees, setupCustomizations } from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings' import { getSettings } from '@/stores/settings'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
@ -196,7 +196,6 @@ import {
Breadcrumbs, Breadcrumbs,
call, call,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
@ -226,9 +225,8 @@ const lead = createResource({
$dialog, $dialog,
$socket, $socket,
router, router,
toast,
updateField, updateField,
createToast: toast.create, createToast,
deleteDoc: deleteLead, deleteDoc: deleteLead,
resource: { resource: {
lead, lead,
@ -263,11 +261,20 @@ function updateLead(fieldname, value, callback) {
onSuccess: () => { onSuccess: () => {
lead.reload() lead.reload()
reload.value = true reload.value = true
toast.success(__('Lead updated successfully')) createToast({
title: __('Lead updated'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
callback?.() callback?.()
}, },
onError: (err) => { onError: (err) => {
toast.error(__(err.messages?.[0] || 'Error updating lead')) createToast({
title: __('Error updating lead'),
text: __(err.messages?.[0]),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
}, },
}) })
} }
@ -275,7 +282,12 @@ function updateLead(fieldname, value, callback) {
function validateRequired(fieldname, value) { function validateRequired(fieldname, value) {
let meta = lead.data.fields_meta || {} let meta = lead.data.fields_meta || {}
if (meta[fieldname]?.reqd && !value) { if (meta[fieldname]?.reqd && !value) {
toast.error(__('{0} is a required field', [meta[fieldname].label])) createToast({
title: __('Error Updating Lead'),
text: __('{0} is a required field', [meta[fieldname].label]),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
return true return true
} }
return false return false
@ -421,12 +433,22 @@ const existingOrganization = ref('')
async function convertToDeal() { async function convertToDeal() {
if (existingContactChecked.value && !existingContact.value) { if (existingContactChecked.value && !existingContact.value) {
toast.error(__('Please select an existing contact')) createToast({
title: __('Error'),
text: __('Please select an existing contact'),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
return return
} }
if (existingOrganizationChecked.value && !existingOrganization.value) { if (existingOrganizationChecked.value && !existingOrganization.value) {
toast.error(__('Please select an existing organization')) createToast({
title: __('Error'),
text: __('Please select an existing organization'),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
return return
} }

View File

@ -165,7 +165,7 @@ import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { formatDate, timeAgo } from '@/utils' import { formatDate, timeAgo, createToast } from '@/utils'
import { import {
Breadcrumbs, Breadcrumbs,
Avatar, Avatar,
@ -179,7 +179,6 @@ import {
createDocumentResource, createDocumentResource,
usePageMeta, usePageMeta,
createResource, createResource,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { h, computed, ref } from 'vue' import { h, computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
@ -208,6 +207,17 @@ const organization = createDocumentResource({
auto: true, auto: true,
}) })
async function updateField(fieldname, value) {
await organization.setValue.submit({
[fieldname]: value,
})
createToast({
title: __('Organization updated'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
}
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
let items = [{ label: __('Organizations'), route: { name: 'Organizations' } }] let items = [{ label: __('Organizations'), route: { name: 'Organizations' } }]
@ -292,7 +302,12 @@ async function deleteOrganization() {
} }
function openWebsite() { function openWebsite() {
if (!organization.doc.website) toast.error(__('No website found')) if (!organization.doc.website)
createToast({
title: __('Website not found'),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
else window.open(organization.doc.website, '_blank') else window.open(organization.doc.website, '_blank')
} }

View File

@ -192,7 +192,7 @@ import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { formatDate, timeAgo } from '@/utils' import { formatDate, timeAgo, createToast } from '@/utils'
import { import {
Tooltip, Tooltip,
Breadcrumbs, Breadcrumbs,
@ -205,7 +205,6 @@ import {
createDocumentResource, createDocumentResource,
usePageMeta, usePageMeta,
createResource, createResource,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { h, computed, ref } from 'vue' import { h, computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
@ -250,6 +249,17 @@ const organization = createDocumentResource({
}, },
}) })
async function updateField(fieldname, value) {
await organization.setValue.submit({
[fieldname]: value,
})
createToast({
title: __('Organization updated'),
icon: 'check',
iconClasses: 'text-ink-green-3',
})
}
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
let items = [{ label: __('Organizations'), route: { name: 'Organizations' } }] let items = [{ label: __('Organizations'), route: { name: 'Organizations' } }]
@ -338,7 +348,12 @@ function website(url) {
} }
function openWebsite() { function openWebsite() {
if (!organization.doc.website) toast.error(__('No website found')) if (!organization.doc.website)
createToast({
title: __('Website not found'),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
else window.open(organization.doc.website, '_blank') else window.open(organization.doc.website, '_blank')
} }

View File

@ -7,6 +7,13 @@ import { getMeta } from '@/stores/meta'
import { toast, dayjsLocal, dayjs } from 'frappe-ui' import { toast, dayjsLocal, dayjs } from 'frappe-ui'
import { h } from 'vue' import { h } from 'vue'
export function createToast(options) {
toast({
position: 'bottom-right',
...options,
})
}
export function formatTime(seconds) { export function formatTime(seconds) {
const days = Math.floor(seconds / (3600 * 24)) const days = Math.floor(seconds / (3600 * 24))
const hours = Math.floor((seconds % (3600 * 24)) / 3600) const hours = Math.floor((seconds % (3600 * 24)) / 3600)
@ -202,20 +209,34 @@ export async function setupListCustomizations(data, obj = {}) {
return { actions, bulkActions } return { actions, bulkActions }
} }
export function errorMessage(title, message) {
createToast({
title: title || 'Error',
text: message,
icon: 'x',
iconClasses: 'text-ink-red-4',
})
}
export function copyToClipboard(text) { export function copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(showSuccessAlert) navigator.clipboard.writeText(text).then(show_success_alert)
} else { } else {
let input = document.createElement('textarea') let input = document.createElement('textarea')
document.body.appendChild(input) document.body.appendChild(input)
input.value = text input.value = text
input.select() input.select()
document.execCommand('copy') document.execCommand('copy')
showSuccessAlert() show_success_alert()
document.body.removeChild(input) document.body.removeChild(input)
} }
function showSuccessAlert() { function show_success_alert() {
toast.success(__('Copied to clipboard')) createToast({
title: 'Copied to clipboard',
text: text,
icon: 'check',
iconClasses: 'text-ink-green-3',
})
} }
} }

View File

@ -18,7 +18,13 @@ export default defineConfig({
sourcemap: true, sourcemap: true,
}, },
}), }),
vue(), vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('Lucide'),
},
},
}),
vueJsx(), vueJsx(),
VitePWA({ VitePWA({
registerType: 'autoUpdate', registerType: 'autoUpdate',