Merge pull request #207 from frappe/develop

chore: Merge develop to main
This commit is contained in:
Shariq Ansari 2024-05-30 22:00:01 +05:30 committed by GitHub
commit 96af77bf78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1051 additions and 695 deletions

View File

@ -106,6 +106,8 @@ class CRMDeal(Document):
"""
Find an SLA to apply to the deal.
"""
if self.sla: return
sla = get_sla(self)
if not sla:
self.first_responded_on = None

View File

@ -6,27 +6,7 @@ from frappe.model.document import Document
class CRMFormScript(Document):
def validate(self):
self.check_if_duplicate()
def check_if_duplicate(self):
"""Check if there is already a script for this doctype"""
if self.dt and self.enabled:
filters = {
"dt": self.dt,
"view": self.view,
"enabled": 1,
}
if self.name:
filters["name"] = ["!=", self.name]
if frappe.db.exists("CRM Form Script", filters):
frappe.throw(
frappe._(
"Script already exists for this doctype and is enabled"
),
frappe.DuplicateEntryError,
)
pass
def get_form_script(dt, view="Form"):
"""Returns the form script for the given doctype"""
@ -37,11 +17,10 @@ def get_form_script(dt, view="Form"):
.where(FormScript.dt == dt)
.where(FormScript.view == view)
.where(FormScript.enabled == 1)
.limit(1)
)
doc = query.run(as_dict=True)
if doc:
return doc[0].script
return [d.script for d in doc] if len(doc) > 1 else doc[0].script
else:
return None

View File

@ -232,6 +232,8 @@ class CRMLead(Document):
"""
Find an SLA to apply to the lead.
"""
if self.sla: return
sla = get_sla(self)
if not sla:
self.first_responded_on = None

View File

@ -41,9 +41,11 @@ class CustomEmailTemplate(EmailTemplate):
rows = [
"name",
"enabled",
"use_html",
"reference_doctype",
"subject",
"response",
"response_html",
"modified",
]
return {'columns': columns, 'rows': rows}

View File

@ -27,6 +27,7 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.5",
"vite": "^4.4.9",

View File

@ -102,11 +102,11 @@
>
<div
v-if="title == 'Notes'"
class="activity grid grid-cols-1 gap-4 px-4 pb-3 sm:px-10 sm:pb-5 lg:grid-cols-2 xl:grid-cols-3"
class="grid grid-cols-1 gap-4 px-4 pb-3 sm:px-10 sm:pb-5 lg:grid-cols-2 xl:grid-cols-3"
>
<div
v-for="note in activities"
class="group flex h-48 cursor-pointer flex-col justify-between gap-2 rounded-md bg-gray-50 px-4 py-3 hover:bg-gray-100"
class="activity group flex h-48 cursor-pointer flex-col justify-between gap-2 rounded-md bg-gray-50 px-4 py-3 hover:bg-gray-100"
@click="showNote(note)"
>
<div class="flex items-center justify-between">
@ -156,10 +156,10 @@
</div>
</div>
</div>
<div v-else-if="title == 'Comments'" class="activity pb-5">
<div v-else-if="title == 'Comments'" class="pb-5">
<div v-for="(comment, i) in activities">
<div
class="grid grid-cols-[30px_minmax(auto,_1fr)] sm:gap-4 gap-2 px-4 sm:px-10"
class="activity grid grid-cols-[30px_minmax(auto,_1fr)] gap-2 px-4 sm:gap-4 sm:px-10"
>
<div
class="relative flex justify-center after:absolute after:left-[50%] after:top-0 after:-z-10 after:border-l after:border-gray-200"
@ -214,13 +214,10 @@
</div>
</div>
</div>
<div
v-else-if="title == 'Tasks'"
class="activity px-4 pb-3 sm:px-10 sm:pb-5"
>
<div v-else-if="title == 'Tasks'" class="px-4 pb-3 sm:px-10 sm:pb-5">
<div v-for="(task, i) in activities">
<div
class="flex cursor-pointer gap-6 rounded p-2.5 duration-300 ease-in-out hover:bg-gray-50"
class="activity flex cursor-pointer gap-6 rounded p-2.5 duration-300 ease-in-out hover:bg-gray-50"
@click="showTask(task)"
>
<div class="flex flex-1 flex-col gap-1.5 text-base">
@ -313,7 +310,7 @@
<div v-else-if="title == 'Calls'" class="activity">
<div v-for="(call, i) in activities">
<div
class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-4 sm:px-10"
class="activity grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-4 sm:px-10"
>
<div
class="relative flex justify-center after:absolute after:left-[50%] after:top-0 after:-z-10 after:border-l after:border-gray-200"
@ -416,283 +413,341 @@
</div>
</div>
</div>
<div v-else v-for="(activity, i) in activities" class="activity">
<div
v-else
v-for="(activity, i) in activities"
class="activity px-4 sm:px-10"
:class="
title == 'Activity'
? 'grid grid-cols-[30px_minmax(auto,_1fr)] gap-2 sm:gap-4'
: ''
"
>
<div
class="px-4 sm:px-10"
:class="
title == 'Activity'
? 'grid grid-cols-[30px_minmax(auto,_1fr)] sm:gap-4 gap-2'
: ''
"
v-if="title == 'Activity'"
class="relative flex justify-center before:absolute before:left-[50%] before:top-0 before:-z-10 before:border-l before:border-gray-200"
:class="[
i != activities.length - 1 ? 'before:h-full' : 'before:h-4',
activity.other_versions
? 'after:translate-y-[calc(-50% - 4px)] after:absolute after:bottom-9 after:left-[50%] after:top-0 after:-z-10 after:w-8 after:rounded-bl-xl after:border-b after:border-l after:border-gray-200'
: '',
]"
>
<div
v-if="title == 'Activity'"
class="relative flex justify-center before:absolute before:left-[50%] before:top-0 before:-z-10 before:border-l before:border-gray-200"
:class="[
i != activities.length - 1 ? 'before:h-full' : 'before:h-4',
activity.other_versions
? 'after:translate-y-[calc(-50% - 4px)] after:absolute after:bottom-9 after:left-[50%] after:top-0 after:-z-10 after:w-8 after:rounded-bl-xl after:border-b after:border-l after:border-gray-200'
: '',
]"
class="z-10 flex h-7 w-7 items-center justify-center rounded bg-gray-100"
:class="{
'mt-3': [
'communication',
'incoming_call',
'outgoing_call',
].includes(activity.activity_type),
'bg-white': ['added', 'removed', 'changed'].includes(
activity.activity_type
),
}"
>
<div
class="z-10 flex h-7 w-7 items-center justify-center rounded bg-gray-100"
:class="{
'mt-3': [
'communication',
'incoming_call',
'outgoing_call',
].includes(activity.activity_type),
'bg-white': ['added', 'removed', 'changed'].includes(
activity.activity_type
),
}"
>
<component
:is="activity.icon"
:class="
['added', 'removed', 'changed'].includes(activity.activity_type)
? 'text-gray-500'
: 'text-gray-800'
"
/>
</div>
</div>
<div v-if="activity.activity_type == 'communication'" class="pb-6">
<div
class="cursor-pointer rounded-md bg-gray-50 p-3 text-base leading-6 transition-all duration-300 ease-in-out"
>
<div class="mb-1 flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<UserAvatar :user="activity.data.sender" size="md" />
<span>{{ activity.data.sender_full_name }}</span>
<span>&middot;</span>
<Tooltip
:text="dateFormat(activity.creation, dateTooltipFormat)"
>
<div class="text-sm text-gray-600">
{{ __(timeAgo(activity.creation)) }}
</div>
</Tooltip>
<Badge
v-if="activity.communication_type == 'Automated Message'"
:label="__('Notification')"
variant="subtle"
theme="green"
/>
</div>
<div class="flex gap-0.5">
<Tooltip :text="__('Reply')">
<Button
variant="ghost"
class="text-gray-700"
@click="reply(activity.data)"
>
<ReplyIcon class="h-4 w-4" />
</Button>
</Tooltip>
<Tooltip :text="__('Reply All')">
<Button
variant="ghost"
class="text-gray-700"
@click="reply(activity.data, true)"
>
<ReplyAllIcon class="h-4 w-4" />
</Button>
</Tooltip>
</div>
</div>
<div class="text-sm leading-5 text-gray-600">
{{ activity.data.subject }}
</div>
<div class="mb-3 text-sm leading-5 text-gray-600">
<span class="mr-1 text-2xs font-bold text-gray-500">
{{ __('TO') }}:
</span>
<span>{{ activity.data.recipients }}</span>
<span v-if="activity.data.cc">, </span>
<span
v-if="activity.data.cc"
class="mr-1 text-2xs font-bold text-gray-500"
>
{{ __('CC') }}:
</span>
<span v-if="activity.data.cc">{{ activity.data.cc }}</span>
<span v-if="activity.data.bcc">, </span>
<span
v-if="activity.data.bcc"
class="mr-1 text-2xs font-bold text-gray-500"
>
{{ __('BCC') }}:
</span>
<span v-if="activity.data.bcc">{{ activity.data.bcc }}</span>
</div>
<FadedScrollableDiv
:maskHeight="30"
class="email-content prose-f max-h-[500px] overflow-y-auto"
v-html="activity.data.content"
/>
<div class="flex flex-wrap gap-2">
<AttachmentItem
v-for="a in activity.data.attachments"
:key="a.file_url"
:label="a.file_name"
:url="a.file_url"
/>
</div>
</div>
<component
:is="activity.icon"
:class="
['added', 'removed', 'changed'].includes(activity.activity_type)
? 'text-gray-500'
: 'text-gray-800'
"
/>
</div>
</div>
<div v-if="activity.activity_type == 'communication'" class="pb-6">
<div
class="mb-4"
:id="activity.name"
v-else-if="activity.activity_type == 'comment'"
class="cursor-pointer rounded-md bg-gray-50 p-3 text-base leading-6 transition-all duration-300 ease-in-out"
>
<div
class="mb-0.5 flex items-start justify-stretch gap-2 py-1.5 text-base"
>
<div class="inline-flex flex-wrap gap-1 text-gray-600">
<span class="font-medium text-gray-800">
{{ activity.owner_name }}
</span>
<span>{{ __('added a') }}</span>
<span class="max-w-xs truncate font-medium text-gray-800">
{{ __('comment') }}
</span>
</div>
<div class="ml-auto whitespace-nowrap">
<div class="mb-1 flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<UserAvatar :user="activity.data.sender" size="md" />
<span>{{ activity.data.sender_full_name }}</span>
<span>&middot;</span>
<Tooltip :text="dateFormat(activity.creation, dateTooltipFormat)">
<div class="text-sm text-gray-600">
{{ __(timeAgo(activity.creation)) }}
</div>
</Tooltip>
</div>
</div>
<div
class="cursor-pointer rounded bg-gray-50 px-4 py-3 text-base leading-6 transition-all duration-300 ease-in-out"
>
<div class="prose-f" v-html="activity.content" />
<div
v-if="activity.attachments.length"
class="mt-2 flex flex-wrap gap-2"
>
<AttachmentItem
v-for="a in activity.attachments"
:key="a.file_url"
:label="a.file_name"
:url="a.file_url"
<Badge
v-if="activity.communication_type == 'Automated Message'"
:label="__('Notification')"
variant="subtle"
theme="green"
/>
</div>
<div class="flex gap-0.5">
<Tooltip :text="__('Reply')">
<Button
variant="ghost"
class="text-gray-700"
@click="reply(activity.data)"
>
<ReplyIcon class="h-4 w-4" />
</Button>
</Tooltip>
<Tooltip :text="__('Reply All')">
<Button
variant="ghost"
class="text-gray-700"
@click="reply(activity.data, true)"
>
<ReplyAllIcon class="h-4 w-4" />
</Button>
</Tooltip>
</div>
</div>
<div class="text-sm leading-5 text-gray-600">
{{ activity.data.subject }}
</div>
<div class="mb-3 text-sm leading-5 text-gray-600">
<span class="mr-1 text-2xs font-bold text-gray-500">
{{ __('TO') }}:
</span>
<span>{{ activity.data.recipients }}</span>
<span v-if="activity.data.cc">, </span>
<span
v-if="activity.data.cc"
class="mr-1 text-2xs font-bold text-gray-500"
>
{{ __('CC') }}:
</span>
<span v-if="activity.data.cc">{{ activity.data.cc }}</span>
<span v-if="activity.data.bcc">, </span>
<span
v-if="activity.data.bcc"
class="mr-1 text-2xs font-bold text-gray-500"
>
{{ __('BCC') }}:
</span>
<span v-if="activity.data.bcc">{{ activity.data.bcc }}</span>
</div>
<EmailContent :content="activity.data.content" />
<div class="flex flex-wrap gap-2">
<AttachmentItem
v-for="a in activity.data.attachments"
:key="a.file_url"
:label="a.file_name"
:url="a.file_url"
/>
</div>
</div>
</div>
<div
class="mb-4"
:id="activity.name"
v-else-if="activity.activity_type == 'comment'"
>
<div
class="mb-0.5 flex items-start justify-stretch gap-2 py-1.5 text-base"
>
<div class="inline-flex flex-wrap gap-1 text-gray-600">
<span class="font-medium text-gray-800">
{{ activity.owner_name }}
</span>
<span>{{ __('added a') }}</span>
<span class="max-w-xs truncate font-medium text-gray-800">
{{ __('comment') }}
</span>
</div>
<div class="ml-auto whitespace-nowrap">
<Tooltip :text="dateFormat(activity.creation, dateTooltipFormat)">
<div class="text-sm text-gray-600">
{{ __(timeAgo(activity.creation)) }}
</div>
</Tooltip>
</div>
</div>
<div
v-else-if="
activity.activity_type == 'incoming_call' ||
activity.activity_type == 'outgoing_call'
"
class="mb-3 flex flex-col gap-3 rounded-md bg-gray-50 p-4 sm:max-w-[70%]"
class="cursor-pointer rounded bg-gray-50 px-4 py-3 text-base leading-6 transition-all duration-300 ease-in-out"
>
<div class="flex items-center justify-between">
<div>
<div class="prose-f" v-html="activity.content" />
<div
v-if="activity.attachments.length"
class="mt-2 flex flex-wrap gap-2"
>
<AttachmentItem
v-for="a in activity.attachments"
:key="a.file_url"
:label="a.file_name"
:url="a.file_url"
/>
</div>
</div>
</div>
<div
v-else-if="
activity.activity_type == 'incoming_call' ||
activity.activity_type == 'outgoing_call'
"
class="mb-3 flex flex-col gap-3 rounded-md bg-gray-50 p-4 sm:max-w-[70%]"
>
<div class="flex items-center justify-between">
<div>
{{
activity.type == 'Incoming'
? __('Inbound Call')
: __('Outbound Call')
}}
</div>
<div>
<Tooltip :text="dateFormat(activity.creation, dateTooltipFormat)">
<div class="text-sm text-gray-600">
{{ __(timeAgo(activity.creation)) }}
</div>
</Tooltip>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
<DurationIcon class="h-4 w-4 text-gray-600" />
<div class="text-sm text-gray-600">{{ __('Duration') }}</div>
<div class="text-sm">
{{ activity.duration }}
</div>
</div>
<div
v-if="activity.recording_url"
class="flex cursor-pointer select-none items-center gap-1"
@click="activity.show_recording = !activity.show_recording"
>
<PlayIcon class="h-4 w-4 text-gray-600" />
<div class="text-sm text-gray-600">
{{
activity.type == 'Incoming'
? __('Inbound Call')
: __('Outbound Call')
activity.show_recording
? __('Hide Recording')
: __('Listen to Call')
}}
</div>
<div>
<Tooltip :text="dateFormat(activity.creation, dateTooltipFormat)">
<div class="text-sm text-gray-600">
{{ __(timeAgo(activity.creation)) }}
</div>
</Tooltip>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
<DurationIcon class="h-4 w-4 text-gray-600" />
<div class="text-sm text-gray-600">{{ __('Duration') }}</div>
<div class="text-sm">
{{ activity.duration }}
</div>
</div>
<div
v-if="activity.recording_url"
class="flex cursor-pointer select-none items-center gap-1"
@click="activity.show_recording = !activity.show_recording"
>
<PlayIcon class="h-4 w-4 text-gray-600" />
<div class="text-sm text-gray-600">
{{
activity.show_recording
? __('Hide Recording')
: __('Listen to Call')
}}
</div>
</div>
</div>
<div
v-if="activity.show_recording && activity.recording_url"
class="flex items-center justify-between rounded border"
>
<audio
class="audio-control"
controls
:src="activity.recording_url"
></audio>
</div>
<div
class="flex items-center justify-between sm:justify-start sm:gap-1"
>
<div class="flex items-center gap-1">
<Avatar
:image="activity.caller.image"
:label="activity.caller.label"
class="sm:h-8 sm:w-8"
/>
<div class="ml-1 flex flex-col gap-1">
<div class="text-xs font-medium sm:text-base">
{{ __(activity.caller.label) }}
</div>
<div class="text-2xs text-gray-600 sm:text-xs">
{{ activity.from }}
</div>
</div>
</div>
<FeatherIcon
name="arrow-right"
class="size-4 text-gray-600 sm:mx-2 sm:size-5"
</div>
<div
v-if="activity.show_recording && activity.recording_url"
class="flex items-center justify-between rounded border"
>
<audio
class="audio-control"
controls
:src="activity.recording_url"
></audio>
</div>
<div
class="flex items-center justify-between sm:justify-start sm:gap-1"
>
<div class="flex items-center gap-1">
<Avatar
:image="activity.caller.image"
:label="activity.caller.label"
class="sm:h-8 sm:w-8"
/>
<div class="flex items-center gap-1">
<Avatar
:image="activity.receiver.image"
:label="activity.receiver.label"
class="sm:h-8 sm:w-8"
/>
<div class="ml-1 flex flex-col gap-1">
<div class="text-xs font-medium sm:text-base">
{{ __(activity.receiver.label) }}
</div>
<div class="text-2xs text-gray-600 sm:text-xs">
{{ activity.to }}
</div>
<div class="ml-1 flex flex-col gap-1">
<div class="text-xs font-medium sm:text-base">
{{ __(activity.caller.label) }}
</div>
<div class="text-2xs text-gray-600 sm:text-xs">
{{ activity.from }}
</div>
</div>
</div>
<FeatherIcon
name="arrow-right"
class="size-4 text-gray-600 sm:mx-2 sm:size-5"
/>
<div class="flex items-center gap-1">
<Avatar
:image="activity.receiver.image"
:label="activity.receiver.label"
class="sm:h-8 sm:w-8"
/>
<div class="ml-1 flex flex-col gap-1">
<div class="text-xs font-medium sm:text-base">
{{ __(activity.receiver.label) }}
</div>
<div class="text-2xs text-gray-600 sm:text-xs">
{{ activity.to }}
</div>
</div>
</div>
</div>
<div v-else class="mb-4 flex flex-col gap-5 py-1.5">
<div class="flex items-start justify-stretch gap-2 text-base">
<div class="inline-flex flex-wrap gap-1 text-gray-600">
<span class="font-medium text-gray-800">
{{ activity.owner_name }}
</span>
<span v-if="activity.type">{{ __(activity.type) }}</span>
</div>
<div v-else class="mb-4 flex flex-col gap-5 py-1.5">
<div class="flex items-start justify-stretch gap-2 text-base">
<div class="inline-flex flex-wrap gap-1 text-gray-600">
<span class="font-medium text-gray-800">
{{ activity.owner_name }}
</span>
<span v-if="activity.type">{{ __(activity.type) }}</span>
<span
v-if="activity.data.field_label"
class="max-w-xs truncate font-medium text-gray-800"
>
{{ __(activity.data.field_label) }}
</span>
<span v-if="activity.value">{{ __(activity.value) }}</span>
<span
v-if="activity.data.old_value"
class="max-w-xs font-medium text-gray-800"
>
<div
class="flex items-center gap-1"
v-if="activity.options == 'User'"
>
<UserAvatar :user="activity.data.old_value" size="xs" />
{{ getUser(activity.data.old_value).full_name }}
</div>
<div class="truncate" v-else>
{{ activity.data.old_value }}
</div>
</span>
<span v-if="activity.to">{{ __('to') }}</span>
<span
v-if="activity.data.value"
class="max-w-xs font-medium text-gray-800"
>
<div
class="flex items-center gap-1"
v-if="activity.options == 'User'"
>
<UserAvatar :user="activity.data.value" size="xs" />
{{ getUser(activity.data.value).full_name }}
</div>
<div class="truncate" v-else>
{{ activity.data.value }}
</div>
</span>
</div>
<div class="ml-auto whitespace-nowrap">
<Tooltip :text="dateFormat(activity.creation, dateTooltipFormat)">
<div class="text-sm text-gray-600">
{{ __(timeAgo(activity.creation)) }}
</div>
</Tooltip>
</div>
</div>
<div
v-if="activity.other_versions && activity.show_others"
v-for="activity in activity.other_versions"
class="flex items-start justify-stretch gap-2 text-base"
>
<div class="flex items-start gap-1 text-gray-600">
<div class="flex flex-1 items-center gap-1">
<span
v-if="activity.data.field_label"
class="max-w-xs truncate font-medium text-gray-800"
class="max-w-xs truncate text-gray-600"
>
{{ __(activity.data.field_label) }}
</span>
<span v-if="activity.value">{{ __(activity.value) }}</span>
<FeatherIcon
name="arrow-right"
class="mx-1 h-4 w-4 text-gray-600"
/>
</div>
<div class="flex flex-wrap items-center gap-1">
<span v-if="activity.type">{{
startCase(__(activity.type))
}}</span>
<span
v-if="activity.data.old_value"
class="max-w-xs font-medium text-gray-800"
@ -725,97 +780,33 @@
</div>
</span>
</div>
<div class="ml-auto whitespace-nowrap">
<Tooltip :text="dateFormat(activity.creation, dateTooltipFormat)">
<div class="text-sm text-gray-600">
{{ __(timeAgo(activity.creation)) }}
</div>
</Tooltip>
</div>
</div>
<div
v-if="activity.other_versions && activity.show_others"
v-for="activity in activity.other_versions"
class="flex items-start justify-stretch gap-2 text-base"
<div class="ml-auto whitespace-nowrap">
<Tooltip :text="dateFormat(activity.creation, dateTooltipFormat)">
<div class="text-sm text-gray-600">
{{ __(timeAgo(activity.creation)) }}
</div>
</Tooltip>
</div>
</div>
<div v-if="activity.other_versions">
<Button
:label="
activity.show_others
? __('Hide all Changes')
: __('Show all Changes')
"
variant="outline"
@click="activity.show_others = !activity.show_others"
>
<div class="flex items-start gap-1 text-gray-600">
<div class="flex flex-1 items-center gap-1">
<span
v-if="activity.data.field_label"
class="max-w-xs truncate text-gray-600"
>
{{ __(activity.data.field_label) }}
</span>
<FeatherIcon
name="arrow-right"
class="mx-1 h-4 w-4 text-gray-600"
/>
</div>
<div class="flex flex-wrap items-center gap-1">
<span v-if="activity.type">{{
startCase(__(activity.type))
}}</span>
<span
v-if="activity.data.old_value"
class="max-w-xs font-medium text-gray-800"
>
<div
class="flex items-center gap-1"
v-if="activity.options == 'User'"
>
<UserAvatar :user="activity.data.old_value" size="xs" />
{{ getUser(activity.data.old_value).full_name }}
</div>
<div class="truncate" v-else>
{{ activity.data.old_value }}
</div>
</span>
<span v-if="activity.to">{{ __('to') }}</span>
<span
v-if="activity.data.value"
class="max-w-xs font-medium text-gray-800"
>
<div
class="flex items-center gap-1"
v-if="activity.options == 'User'"
>
<UserAvatar :user="activity.data.value" size="xs" />
{{ getUser(activity.data.value).full_name }}
</div>
<div class="truncate" v-else>
{{ activity.data.value }}
</div>
</span>
</div>
</div>
<div class="ml-auto whitespace-nowrap">
<Tooltip :text="dateFormat(activity.creation, dateTooltipFormat)">
<div class="text-sm text-gray-600">
{{ __(timeAgo(activity.creation)) }}
</div>
</Tooltip>
</div>
</div>
<div v-if="activity.other_versions">
<Button
:label="
activity.show_others
? __('Hide all Changes')
: __('Show all Changes')
"
variant="outline"
@click="activity.show_others = !activity.show_others"
>
<template #suffix>
<FeatherIcon
:name="activity.show_others ? 'chevron-up' : 'chevron-down'"
class="h-4 text-gray-600"
/>
</template>
</Button>
</div>
<template #suffix>
<FeatherIcon
:name="activity.show_others ? 'chevron-up' : 'chevron-down'"
class="h-4 text-gray-600"
/>
</template>
</Button>
</div>
</div>
</div>
@ -891,6 +882,7 @@
/>
</template>
<script setup>
import EmailContent from '@/components/EmailContent.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
import EmailIcon from '@/components/Icons/EmailIcon.vue'
@ -1404,79 +1396,4 @@ nextTick(() => {
.audio-control::-webkit-media-controls-panel {
background-color: white;
}
/* email content */
.email-content {
word-break: break-word;
}
:deep(.email-content
:is(:where(table):not(:where([class~='not-prose'], [class~='not-prose']
*)))) {
table-layout: auto;
}
:deep(.email-content
:where(table):not(:where([class~='not-prose'], [class~='not-prose'] *))) {
width: unset;
table-layout: auto;
text-align: unset;
margin-top: unset;
margin-bottom: unset;
font-size: unset;
line-height: unset;
}
/* tr */
:deep(.email-content
:where(tbody tr):not(:where([class~='not-prose'], [class~='not-prose']
*))) {
border-bottom-width: 0;
border-bottom-color: transparent;
}
/* td */
:deep(.email-content
:is(:where(td):not(:where([class~='not-prose'], [class~='not-prose'] *)))) {
position: unset;
border-width: 0;
border-color: transparent;
padding: 0;
}
:deep(.email-content
:where(tbody td):not(:where([class~='not-prose'], [class~='not-prose']
*))) {
vertical-align: revert;
}
/* image */
:deep(.email-content
:is(:where(img):not(:where([class~='not-prose'], [class~='not-prose']
*)))) {
border-width: 0;
}
:deep(.email-content
:where(img):not(:where([class~='not-prose'], [class~='not-prose'] *))) {
margin: 0;
}
/* before & after */
:deep(.email-content
:where(blockquote
p:first-of-type):not(:where([class~='not-prose'], [class~='not-prose']
*))::before) {
content: none;
}
:deep(.email-content
:where(blockquote
p:last-of-type):not(:where([class~='not-prose'], [class~='not-prose']
*))::after) {
content: none;
}
</style>

View File

@ -1,5 +1,6 @@
<template>
<Button
v-if="normalActions.length && !isMobileView"
v-for="action in normalActions"
:label="action.label"
@click="action.onClick()"
@ -11,6 +12,22 @@
<Dropdown v-if="groupedActions.length" :options="groupedActions">
<Button icon="more-horizontal" />
</Dropdown>
<div
v-if="groupedWithLabelActions.length && !isMobileView"
v-for="g in groupedWithLabelActions"
:key="g.label"
>
<Dropdown :options="g.action" v-slot="{ open }">
<Button :label="g.label">
<template #suffix>
<FeatherIcon
:name="open ? 'chevron-up' : 'chevron-down'"
class="h-4"
/>
</template>
</Button>
</Dropdown>
</div>
</template>
<script setup>
@ -25,9 +42,32 @@ const props = defineProps({
},
})
const normalActions = computed(() => {
return props.actions.filter((action) => !action.group)
})
const groupedWithLabelActions = computed(() => {
let _actions = []
props.actions
.filter((action) => action.buttonLabel && action.group)
.forEach((action) => {
let groupIndex = _actions.findIndex((a) => a.label === action.buttonLabel)
if (groupIndex > -1) {
_actions[groupIndex].action.push(action)
} else {
_actions.push({
label: action.buttonLabel,
action: [action],
})
}
})
return _actions
})
const groupedActions = computed(() => {
let _actions = []
let _normalActions = props.actions.filter((action) => !action.group)
let _normalActions = normalActions.value
if (isMobileView.value && _normalActions.length) {
_actions.push({
group: __('Actions'),
@ -39,17 +79,14 @@ const groupedActions = computed(() => {
})),
})
}
if (isMobileView.value && groupedWithLabelActions.value.length) {
groupedWithLabelActions.value.map((group) => {
group.action.forEach((action) => _actions.push(action))
})
}
_actions = _actions.concat(
props.actions.filter((action) => action.group)
props.actions.filter((action) => action.group && !action.buttonLabel)
)
return _actions
})
const normalActions = computed(() => {
let _actions = props.actions.filter((action) => !action.group)
if (isMobileView.value && _actions.length) {
return []
}
return _actions
})
</script>

View File

@ -0,0 +1,127 @@
<template>
<iframe
ref="iframeRef"
:srcdoc="htmlContent"
class="prose-f block h-screen max-h-[500px] w-full"
style="
mask-image: linear-gradient(
to bottom,
black calc(100% - 30px),
transparent 100%
);
"
/>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
content: {
type: String,
required: true,
},
})
const files = import.meta.globEager('/src/index.css')
const css = files['/src/index.css'].default
const iframeRef = ref(null)
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<style>
${css}
.email-content {
word-break: break-word;
}
.email-content
:is(:where(table):not(:where([class~='not-prose'], [class~='not-prose']
*))) {
table-layout: auto;
}
.email-content
:where(table):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
width: unset;
table-layout: auto;
text-align: unset;
margin-top: unset;
margin-bottom: unset;
font-size: unset;
line-height: unset;
}
/* tr */
.email-content
:where(tbody tr):not(:where([class~='not-prose'], [class~='not-prose']
*)) {
border-bottom-width: 0;
border-bottom-color: transparent;
}
/* td */
.email-content
:is(:where(td):not(:where([class~='not-prose'], [class~='not-prose'] *))) {
position: unset;
border-width: 0;
border-color: transparent;
padding: 0;
}
.email-content
:where(tbody td):not(:where([class~='not-prose'], [class~='not-prose']
*)) {
vertical-align: revert;
}
/* image */
.email-content
:is(:where(img):not(:where([class~='not-prose'], [class~='not-prose']
*))) {
border-width: 0;
}
.email-content
:where(img):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
margin: 0;
}
/* before & after */
.email-content
:where(blockquote
p:first-of-type):not(:where([class~='not-prose'], [class~='not-prose']
*))::before {
content: none;
}
.email-content
:where(blockquote
p:last-of-type):not(:where([class~='not-prose'], [class~='not-prose']
*))::after {
content: none;
}
</style>
</head>
<body>
<div ref="emailContentRef" class="email-content prose-f">${props.content}</div>
</body>
</html>
`
watch(iframeRef, (iframe) => {
if (iframe) {
iframe.onload = () => {
const emailContent =
iframe.contentWindow.document.querySelector('.email-content')
iframe.style.height = emailContent.offsetHeight + 25 + 'px'
}
}
})
</script>

View File

@ -62,7 +62,7 @@
<template v-slot:editor="{ editor }">
<EditorContent
:class="[
editable && 'sm:mx-10 mx-4 max-h-[50vh] overflow-y-auto border-t py-3',
editable && 'sm:mx-10 mx-4 max-h-[35vh] overflow-y-auto border-t py-3',
]"
:editor="editor"
/>

View File

@ -1,16 +1,21 @@
<template>
<div
<component
:is="props.as || 'div'"
ref="scrollableDiv"
:style="`maskImage: ${maskStyle}`"
@scroll="updateMaskStyle"
>
<slot></slot>
</div>
</component>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const props = defineProps({
as: {
type: String,
default: 'div',
},
maskLength: {
type: Number,
default: 30,

View File

@ -15,11 +15,13 @@
</template>
</Button>
<Tooltip v-if="filters?.size" :text="__('Clear all Filter')">
<Button
class="rounded-l-none border-l"
icon="x"
@click.stop="clearfilter(false)"
/>
<span>
<Button
class="rounded-l-none border-l"
icon="x"
@click.stop="clearfilter(false)"
/>
</span>
</Tooltip>
</template>
<template #body="{ close }">

View File

@ -231,6 +231,8 @@ watch(pageLengthCount, (val, old_value) => {
const listBulkActionsRef = ref(null)
defineExpose({
customListActions: listBulkActionsRef.value?.customListActions,
customListActions: computed(
() => listBulkActionsRef.value?.customListActions
),
})
</script>

View File

@ -227,6 +227,8 @@ watch(pageLengthCount, (val, old_value) => {
const listBulkActionsRef = ref(null)
defineExpose({
customListActions: listBulkActionsRef.value?.customListActions,
customListActions: computed(
() => listBulkActionsRef.value?.customListActions
),
})
</script>

View File

@ -269,6 +269,8 @@ watch(pageLengthCount, (val, old_value) => {
const listBulkActionsRef = ref(null)
defineExpose({
customListActions: listBulkActionsRef.value?.customListActions,
customListActions: computed(
() => listBulkActionsRef.value?.customListActions
),
})
</script>

View File

@ -219,6 +219,8 @@ watch(pageLengthCount, (val, old_value) => {
const listBulkActionsRef = ref(null)
defineExpose({
customListActions: listBulkActionsRef.value?.customListActions,
customListActions: computed(
() => listBulkActionsRef.value?.customListActions
),
})
</script>

View File

@ -278,6 +278,8 @@ watch(pageLengthCount, (val, old_value) => {
const listBulkActionsRef = ref(null)
defineExpose({
customListActions: listBulkActionsRef.value?.customListActions,
customListActions: computed(
() => listBulkActionsRef.value?.customListActions
),
})
</script>

View File

@ -212,6 +212,8 @@ watch(pageLengthCount, (val, old_value) => {
const listBulkActionsRef = ref(null)
defineExpose({
customListActions: listBulkActionsRef.value?.customListActions,
customListActions: computed(
() => listBulkActionsRef.value?.customListActions
),
})
</script>

View File

@ -230,6 +230,8 @@ watch(pageLengthCount, (val, old_value) => {
const listBulkActionsRef = ref(null)
defineExpose({
customListActions: listBulkActionsRef.value?.customListActions,
customListActions: computed(
() => listBulkActionsRef.value?.customListActions
),
})
</script>

View File

@ -84,7 +84,9 @@
</div>
</template>
<template
v-if="callLog.type.label == 'Incoming' && !callLog.reference_docname"
v-if="
callLog.doc?.type.label == 'Incoming' && !callLog.doc?.reference_docname
"
#actions
>
<Button
@ -116,11 +118,12 @@ import {
createDocumentResource,
call,
} from 'frappe-ui'
import { getCallLogDetail } from '@/utils/callLog'
import { ref, computed, h, watch } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({
callLog: {
name: {
type: Object,
default: {},
},
@ -130,60 +133,66 @@ const show = defineModel()
const showNoteModal = ref(false)
const router = useRouter()
const callNoteDoc = ref(null)
const callLog = ref({})
const detailFields = computed(() => {
if (!callLog.value.doc) return []
let details = [
{
icon: h(FeatherIcon, {
name: props.callLog.type.icon,
name: callLog.value.doc.type.icon,
class: 'h-3.5 w-3.5',
}),
name: 'type',
value: props.callLog.type.label + ' Call',
value: callLog.value.doc.type.label + ' Call',
},
{
icon: ContactsIcon,
name: 'receiver',
value: {
receiver: props.callLog.receiver,
caller: props.callLog.caller,
receiver: callLog.value.doc.receiver,
caller: callLog.value.doc.caller,
},
},
{
icon:
props.callLog.reference_doctype == 'CRM Lead' ? LeadsIcon : Dealsicon,
callLog.value.doc.reference_doctype == 'CRM Lead'
? LeadsIcon
: Dealsicon,
name: 'reference_doctype',
value: props.callLog.reference_doctype == 'CRM Lead' ? 'Lead' : 'Deal',
value:
callLog.value.doc.reference_doctype == 'CRM Lead' ? 'Lead' : 'Deal',
link: () => {
if (props.callLog.reference_doctype == 'CRM Lead') {
if (callLog.value.doc.reference_doctype == 'CRM Lead') {
router.push({
name: 'Lead',
params: { leadId: props.callLog.reference_docname },
params: { leadId: callLog.value.doc.reference_docname },
})
} else {
router.push({
name: 'Deal',
params: { dealId: props.callLog.reference_docname },
params: { dealId: callLog.value.doc.reference_docname },
})
}
},
condition: () => callLog.value.doc.reference_docname,
},
{
icon: CalendarIcon,
name: 'creation',
value: props.callLog.creation.label,
tooltip: props.callLog.creation.label,
value: callLog.value.doc.creation.label,
tooltip: callLog.value.doc.creation.label,
},
{
icon: DurationIcon,
name: 'duration',
value: props.callLog.duration.label,
value: callLog.value.doc.duration.label,
},
{
icon: CheckCircleIcon,
name: 'status',
value: props.callLog.status.label,
color: props.callLog.status.color,
value: callLog.value.doc.status.label,
color: callLog.value.doc.status.color,
},
{
icon: h(FeatherIcon, {
@ -191,7 +200,7 @@ const detailFields = computed(() => {
class: 'h-4 w-4 mt-2',
}),
name: 'recording_url',
value: props.callLog.recording_url,
value: callLog.value.doc.recording_url,
},
{
icon: NoteIcon,
@ -200,12 +209,14 @@ const detailFields = computed(() => {
},
]
return details.filter((detail) => detail.value)
return details
.filter((detail) => detail.value)
.filter((detail) => (detail.condition ? detail.condition() : true))
})
function createLead() {
call('crm.fcrm.doctype.crm_call_log.crm_call_log.create_lead_from_call_log', {
call_log: props.callLog,
call_log: callLog.value.doc,
}).then((d) => {
if (d) {
router.push({ name: 'Lead', params: { leadId: d } })
@ -215,12 +226,45 @@ function createLead() {
watch(show, (val) => {
if (val) {
callNoteDoc.value = createDocumentResource({
doctype: 'FCRM Note',
name: props.callLog.note,
fields: ['title', 'content'],
cache: ['note', props.callLog.note],
callLog.value = createDocumentResource({
doctype: 'CRM Call Log',
name: props.name,
fields: [
'name',
'caller',
'receiver',
'duration',
'type',
'status',
'from',
'to',
'note',
'recording_url',
'reference_doctype',
'reference_docname',
'creation',
],
cache: ['call_log', props.name],
auto: true,
transform: (doc) => {
for (const key in doc) {
doc[key] = getCallLogDetail(key, doc)
}
return doc
},
onSuccess: (doc) => {
if (!doc.note) {
callNoteDoc.value = null
return
}
callNoteDoc.value = createDocumentResource({
doctype: 'FCRM Note',
name: doc.note,
fields: ['title', 'content'],
cache: ['note', doc.note],
auto: true,
})
},
})
}
})

View File

@ -55,19 +55,40 @@
{{ __('Content') }}
<span class="text-red-500">*</span>
</div>
<FormControl
v-if="_emailTemplate.use_html"
type="textarea"
variant="outline"
ref="content"
rows="10"
v-model="_emailTemplate.response_html"
:placeholder="
__(
'<p>Dear {{ lead_name }},</p>\n\n<p>This is a reminder for the payment of {{ grand_total }}.</p>\n\n<p>Thanks,</p>\n<p>Frappé</p>'
)
"
/>
<TextEditor
v-else
variant="outline"
ref="content"
editor-class="!prose-sm overflow-auto min-h-[180px] max-h-80 py-1.5 px-2 rounded border border-gray-300 bg-white hover:border-gray-400 hover:shadow-sm focus:bg-white focus:border-gray-500 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400 text-gray-800 transition-colors"
:bubbleMenu="true"
:content="_emailTemplate.response"
@change="(val) => (_emailTemplate.response = val)"
:placeholder="__('Dear {{ lead_name }}, \n\nThis is a reminder for the payment of {{ grand_total }}. \n\nThanks, \nFrappé')"
:placeholder="
__(
'Dear {{ lead_name }}, \n\nThis is a reminder for the payment of {{ grand_total }}. \n\nThanks, \nFrappé'
)
"
/>
</div>
<div>
<Checkbox v-model="_emailTemplate.enabled" :label="__('Enabled')" />
</div>
<div>
<Checkbox v-model="_emailTemplate.use_html" :label="__('Use HTML')" />
</div>
<ErrorMessage :message="__(errorMessage)" />
</div>
</template>

View File

@ -31,7 +31,14 @@
{{ __('Subject: {0}', [template.subject]) }}
</div>
<TextEditor
v-if="template.response"
v-if="template.use_html && template.response_html"
:content="template.response_html"
:editable="false"
editor-class="!prose-sm max-w-none !text-sm text-gray-600 focus:outline-none"
class="flex-1 overflow-hidden"
/>
<TextEditor
v-else-if="template.response"
:content="template.response"
:editable="false"
editor-class="!prose-sm max-w-none !text-sm text-gray-600 focus:outline-none"
@ -97,9 +104,11 @@ const templates = createListResource({
fields: [
'name',
'enabled',
'use_html',
'reference_doctype',
'subject',
'response',
'response_html',
'modified',
'owner',
],

View File

@ -0,0 +1,64 @@
<template>
<FormControl
v-if="filter.type == 'Check'"
:label="filter.label"
type="checkbox"
v-model="filter.value"
@change.stop="updateFilter(filter, $event.target.checked)"
/>
<FormControl
v-else-if="filter.type === 'Select'"
class="form-control cursor-pointer [&_select]:cursor-pointer"
type="select"
v-model="filter.value"
:options="filter.options"
:placeholder="filter.label"
@change.stop="updateFilter(filter, $event.target.value)"
/>
<Link
v-else-if="filter.type === 'Link'"
:value="filter.value"
:doctype="filter.options"
:placeholder="filter.label"
@change="(data) => updateFilter(filter, data)"
/>
<component
v-else-if="['Date', 'Datetime'].includes(filter.type)"
class="border-none"
:is="filter.type === 'Date' ? DatePicker : DatetimePicker"
:value="filter.value"
@change="(v) => updateFilter(filter, v)"
:placeholder="filter.label"
/>
<TextInput
v-else
v-model="filter.value"
type="text"
:placeholder="filter.label"
@input.stop="debouncedFn(filter, $event.target.value)"
/>
</template>
<script setup>
import DatePicker from '@/components/Controls/DatePicker.vue'
import DatetimePicker from '@/components/Controls/DatetimePicker.vue'
import Link from '@/components/Controls/Link.vue'
import { TextInput, FormControl } from 'frappe-ui'
import { useDebounceFn } from '@vueuse/core'
const props = defineProps({
filter: {
type: Object,
required: true,
},
})
const emit = defineEmits(['applyQuickFilter'])
const debouncedFn = useDebounceFn((f, value) => {
emit('applyQuickFilter', f, value)
}, 500)
function updateFilter(f, value) {
emit('applyQuickFilter', f, value)
}
</script>

View File

@ -96,44 +96,9 @@
:key="filter.name"
class="m-1 min-w-36"
>
<FormControl
v-if="filter.type == 'Check'"
:label="filter.label"
type="checkbox"
v-model="filter.value"
@change.stop="applyQuickFilter(filter, $event.target.checked)"
/>
<FormControl
v-else-if="filter.type === 'Select'"
class="form-control cursor-pointer [&_select]:cursor-pointer"
type="select"
v-model="filter.value"
:options="filter.options"
:placeholder="filter.label"
@change.stop="applyQuickFilter(filter, $event.target.value)"
/>
<Link
v-else-if="filter.type === 'Link'"
:value="filter.value"
:doctype="filter.options"
:placeholder="filter.label"
@change="(data) => applyQuickFilter(filter, data)"
/>
<component
v-else-if="['Date', 'Datetime'].includes(filter.type)"
class="border-none"
:is="filter.type === 'Date' ? DatePicker : DatetimePicker"
:value="filter.value"
@change="(v) => applyQuickFilter(filter, v)"
:placeholder="filter.label"
/>
<FormControl
v-else
:value="filter.value"
type="text"
:placeholder="filter.label"
:debounce="500"
@change.stop="applyQuickFilter(filter, $event.target.value)"
<QuickFilterField
:filter="filter"
@applyQuickFilter="(f, v) => applyQuickFilter(f, v)"
/>
</div>
</FadedScrollableDiv>
@ -247,9 +212,7 @@
</Dialog>
</template>
<script setup>
import DatePicker from '@/components/Controls/DatePicker.vue'
import DatetimePicker from '@/components/Controls/DatetimePicker.vue'
import Link from '@/components/Controls/Link.vue'
import QuickFilterField from '@/components/QuickFilterField.vue'
import RefreshIcon from '@/components/Icons/RefreshIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import DuplicateIcon from '@/components/Icons/DuplicateIcon.vue'

View File

@ -53,7 +53,7 @@
<CallLogModal
v-model="showCallLogModal"
v-model:reloadCallLogs="callLogs"
:callLog="callLog"
:name="selectedCallLog"
/>
</template>
@ -64,20 +64,10 @@ import LayoutHeader from '@/components/LayoutHeader.vue'
import ViewControls from '@/components/ViewControls.vue'
import CallLogsListView from '@/components/ListViews/CallLogsListView.vue'
import CallLogModal from '@/components/Modals/CallLogModal.vue'
import {
secondsToDuration,
dateFormat,
dateTooltipFormat,
timeAgo,
} from '@/utils'
import { usersStore } from '@/stores/users'
import { contactsStore } from '@/stores/contacts'
import { getCallLogDetail } from '@/utils/callLog'
import { Breadcrumbs } from 'frappe-ui'
import { computed, ref } from 'vue'
const { getUser } = usersStore()
const { getContact, getLeadContact } = contactsStore()
const breadcrumbs = [{ label: __('Call Logs'), route: { name: 'Call Logs' } }]
const callLogsListView = ref(null)
@ -94,119 +84,17 @@ const rows = computed(() => {
return callLogs.value?.data.data.map((callLog) => {
let _rows = {}
callLogs.value?.data.rows.forEach((row) => {
_rows[row] = callLog[row]
let incoming = callLog.type === 'Incoming'
if (row === 'caller') {
_rows[row] = {
label: incoming
? getContact(callLog.from)?.full_name ||
getLeadContact(callLog.from)?.full_name ||
'Unknown'
: getUser(callLog.caller).full_name,
image: incoming
? getContact(callLog.from)?.image ||
getLeadContact(callLog.from)?.image
: getUser(callLog.caller).user_image,
}
} else if (row === 'receiver') {
_rows[row] = {
label: incoming
? getUser(callLog.receiver).full_name
: getContact(callLog.to)?.full_name ||
getLeadContact(callLog.to)?.full_name ||
'Unknown',
image: incoming
? getUser(callLog.receiver).user_image
: getContact(callLog.to)?.image ||
getLeadContact(callLog.to)?.image,
}
} else if (row === 'duration') {
_rows[row] = {
label: secondsToDuration(callLog.duration),
icon: 'clock',
}
} else if (row === 'type') {
_rows[row] = {
label: callLog.type,
icon: incoming ? 'phone-incoming' : 'phone-outgoing',
}
} else if (row === 'status') {
_rows[row] = {
label: statusLabelMap[callLog.status],
color: statusColorMap[callLog.status],
}
} else if (['modified', 'creation'].includes(row)) {
_rows[row] = {
label: dateFormat(callLog[row], dateTooltipFormat),
timeAgo: __(timeAgo(callLog[row])),
}
}
_rows[row] = getCallLogDetail(row, callLog)
})
return _rows
})
})
const showCallLogModal = ref(false)
const callLog = ref({
name: '',
caller: '',
receiver: '',
duration: '',
type: '',
status: '',
from: '',
to: '',
note: '',
recording_url: '',
reference_doctype: '',
reference_docname: '',
creation: '',
})
const selectedCallLog = ref(null)
function showCallLog(name) {
let d = rows.value?.find((row) => row.name === name)
callLog.value = {
name: d.name,
caller: d.caller,
receiver: d.receiver,
duration: d.duration,
type: d.type,
status: d.status,
from: d.from,
to: d.to,
note: d.note,
recording_url: d.recording_url,
reference_doctype: d.reference_doctype,
reference_docname: d.reference_docname,
creation: d.creation,
}
selectedCallLog.value = name
showCallLogModal.value = true
}
const statusLabelMap = {
Completed: 'Completed',
Initiated: 'Initiated',
Busy: 'Declined',
Failed: 'Failed',
Queued: 'Queued',
Cancelled: 'Cancelled',
Ringing: 'Ringing',
'No Answer': 'Missed Call',
'In Progress': 'In Progress',
}
const statusColorMap = {
Completed: 'green',
Busy: 'orange',
Failed: 'red',
Initiated: 'gray',
Queued: 'gray',
Cancelled: 'gray',
Ringing: 'gray',
'No Answer': 'red',
'In Progress': 'blue',
}
</script>

View File

@ -114,8 +114,10 @@ const showEmailTemplateModal = ref(false)
const emailTemplate = ref({
subject: '',
response: '',
response_html: '',
name: '',
enabled: 1,
use_html: 0,
owner: '',
reference_doctype: 'CRM Deal',
})
@ -125,8 +127,10 @@ function showEmailTemplate(name) {
emailTemplate.value = {
subject: et.subject,
response: et.response,
response_html: et.response_html,
name: et.name,
enabled: et.enabled,
use_html: et.use_html,
owner: et.owner,
reference_doctype: et.reference_doctype,
}

View File

@ -0,0 +1,84 @@
import {
secondsToDuration,
dateFormat,
dateTooltipFormat,
timeAgo,
} from '@/utils'
import { usersStore } from '@/stores/users'
import { contactsStore } from '@/stores/contacts'
const { getUser } = usersStore()
const { getContact, getLeadContact } = contactsStore()
export function getCallLogDetail(row, log) {
let incoming = log.type === 'Incoming'
if (row === 'caller') {
return {
label: incoming
? getContact(log.from)?.full_name ||
getLeadContact(log.from)?.full_name ||
'Unknown'
: getUser(log.caller).full_name,
image: incoming
? getContact(log.from)?.image || getLeadContact(log.from)?.image
: getUser(log.caller).user_image,
}
} else if (row === 'receiver') {
return {
label: incoming
? getUser(log.receiver).full_name
: getContact(log.to)?.full_name ||
getLeadContact(log.to)?.full_name ||
'Unknown',
image: incoming
? getUser(log.receiver).user_image
: getContact(log.to)?.image || getLeadContact(log.to)?.image,
}
} else if (row === 'duration') {
return {
label: secondsToDuration(log.duration),
icon: 'clock',
}
} else if (row === 'type') {
return {
label: log.type,
icon: incoming ? 'phone-incoming' : 'phone-outgoing',
}
} else if (row === 'status') {
return {
label: statusLabelMap[log.status],
color: statusColorMap[log.status],
}
} else if (['modified', 'creation'].includes(row)) {
return {
label: dateFormat(log[row], dateTooltipFormat),
timeAgo: __(timeAgo(log[row])),
}
}
return log[row]
}
export const statusLabelMap = {
Completed: 'Completed',
Initiated: 'Initiated',
Busy: 'Declined',
Failed: 'Failed',
Queued: 'Queued',
Cancelled: 'Cancelled',
Ringing: 'Ringing',
'No Answer': 'Missed Call',
'In Progress': 'In Progress',
}
export const statusColorMap = {
Completed: 'green',
Busy: 'orange',
Failed: 'red',
Initiated: 'gray',
Queued: 'gray',
Cancelled: 'gray',
Ringing: 'gray',
'No Answer': 'red',
'In Progress': 'blue',
}

View File

@ -1,31 +0,0 @@
import { Dialog, ErrorMessage } from 'frappe-ui'
import { h, reactive, ref } from 'vue'
let dialogs = ref([])
export let Dialogs = {
name: 'Dialogs',
render() {
return dialogs.value.map((dialog) => {
return h(
Dialog,
{
options: dialog,
modelValue: dialog.show,
'onUpdate:modelValue': (val) => (dialog.show = val),
},
() => [
h('p', { class: 'text-p-base text-gray-700' }, dialog.message),
h(ErrorMessage, { class: 'mt-2', message: dialog.error }),
]
)
})
},
}
export function createDialog(options) {
let dialog = reactive(options)
dialog.key = `dialog-${Math.random().toString(36).slice(2, 9)}`
dialogs.value.push(dialog)
dialog.show = true
}

View File

@ -0,0 +1,41 @@
import { Dialog, ErrorMessage } from 'frappe-ui'
import { reactive, ref } from 'vue'
let dialogs = ref([])
export let Dialogs = {
name: 'Dialogs',
render() {
return dialogs.value.map((dialog) => (
<Dialog
options={dialog}
modelValue={dialog.show}
onUpdate:modelValue={(val) => (dialog.show = val)}
>
{{
'body-content': () => {
return [
dialog.message && (
<p class="text-p-base text-gray-700">{dialog.message}</p>
),
dialog.html && (
<div v-html={dialog.html} />
),
<ErrorMessage class="mt-2" message={dialog.error} />,
]
},
}}
</Dialog>
))
},
}
export function createDialog(dialogOptions) {
let dialog = reactive(dialogOptions)
dialog.key = 'dialog-' + dialogs.value.length
dialog.show = false
setTimeout(() => {
dialog.show = true
}, 0)
dialogs.value.push(dialog)
}

View File

@ -125,19 +125,56 @@ export function setupAssignees(data) {
}))
}
function getActionsFromScript(script, obj) {
let scriptFn = new Function(script + '\nreturn setupForm')()
let formScript = scriptFn(obj)
return formScript?.actions || []
}
export function setupCustomActions(data, obj) {
if (!data._form_script) return []
let script = new Function(data._form_script + '\nreturn setupForm')()
let formScript = script(obj)
data._customActions = formScript?.actions || []
let actions = []
if (Array.isArray(data._form_script)) {
data._form_script.forEach((script) => {
actions = actions.concat(getActionsFromScript(script, obj))
})
} else {
actions = getActionsFromScript(data._form_script, obj)
}
data._customActions = actions
}
function getActionsFromListScript(script, obj) {
let scriptFn = new Function(script + '\nreturn setupList')()
let listScript = scriptFn(obj)
return {
actions: listScript?.actions || [],
bulk_actions: listScript?.bulk_actions || [],
}
}
export function setupListActions(data, obj = {}) {
if (!data.list_script) return []
let script = new Function(data.list_script + '\nreturn setupList')()
let listScript = script(obj)
data.listActions = listScript?.actions || []
data.bulkActions = listScript?.bulk_actions || []
let actions = []
let bulkActions = []
if (Array.isArray(data.list_script)) {
data.list_script.forEach((script) => {
let _actions = getActionsFromListScript(script, obj)
actions = actions.concat(_actions.actions)
bulkActions = bulkActions.concat(_actions.bulk_actions)
})
} else {
let _actions = getActionsFromListScript(data.list_script, obj)
actions = _actions.actions
bulkActions = _actions.bulk_actions
}
data.listActions = actions
data.bulkActions = bulkActions
}
export function errorMessage(title, message) {

View File

@ -1,5 +1,6 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import path from 'path'
import frappeui from 'frappe-ui/vite'
import { VitePWA } from 'vite-plugin-pwa'
@ -13,6 +14,7 @@ export default defineConfig({
propsDestructure: true,
},
}),
vueJsx(),
VitePWA({
registerType: 'autoUpdate',
devOptions: {

154
yarn.lock
View File

@ -29,7 +29,7 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.24.6":
"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.6":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.6.tgz#ab88da19344445c3d8889af2216606d3329f3ef2"
integrity sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==
@ -42,7 +42,7 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.6.tgz#b3600217688cabb26e25f8e467019e66d71b7ae2"
integrity sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==
"@babel/core@^7.11.1":
"@babel/core@^7.11.1", "@babel/core@^7.23.3":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.6.tgz#8650e0e4b03589ebe886c4e4a60398db0a7ec787"
integrity sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==
@ -167,6 +167,13 @@
dependencies:
"@babel/types" "^7.24.6"
"@babel/helper-module-imports@~7.22.15":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0"
integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==
dependencies:
"@babel/types" "^7.22.15"
"@babel/helper-module-transforms@^7.24.6":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz#22346ed9df44ce84dee850d7433c5b73fab1fe4e"
@ -276,7 +283,7 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88"
integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==
"@babel/parser@^7.24.6":
"@babel/parser@^7.24.4", "@babel/parser@^7.24.6":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.6.tgz#5e030f440c3c6c78d195528c3b688b101a365328"
integrity sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==
@ -381,6 +388,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.0"
"@babel/plugin-syntax-jsx@^7.23.3":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.6.tgz#bcca2964150437f88f65e3679e3d68762287b9c8"
integrity sha512-lWfvAIFNWMlCsU0DRUun2GpFwZdGTukLaHJqRh1JRb80NdAP5Sb1HDHB5X9P9OtgZHQl089UzQkpYlBq2VTPRw==
dependencies:
"@babel/helper-plugin-utils" "^7.24.6"
"@babel/plugin-syntax-logical-assignment-operators@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
@ -437,6 +451,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-typescript@^7.24.6":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.6.tgz#769daf2982d60308bc83d8936eaecb7582463c87"
integrity sha512-TzCtxGgVTEJWWwcYwQhCIQ6WaKlo80/B+Onsk4RRCcYqpYGFcG9etPW94VToGte5AAcxRrhjPUFvUS3Y2qKi4A==
dependencies:
"@babel/helper-plugin-utils" "^7.24.6"
"@babel/plugin-syntax-unicode-sets-regex@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357"
@ -801,6 +822,16 @@
dependencies:
"@babel/helper-plugin-utils" "^7.24.6"
"@babel/plugin-transform-typescript@^7.23.3":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.6.tgz#339c6127a783c32e28a5b591e6c666f899b57db0"
integrity sha512-H0i+hDLmaYYSt6KU9cZE0gb3Cbssa/oxWis7PX4ofQzbvsfix9Lbh8SRk7LCPDlLWJHUiFeHU0qRRpF/4Zv7mQ==
dependencies:
"@babel/helper-annotate-as-pure" "^7.24.6"
"@babel/helper-create-class-features-plugin" "^7.24.6"
"@babel/helper-plugin-utils" "^7.24.6"
"@babel/plugin-syntax-typescript" "^7.24.6"
"@babel/plugin-transform-unicode-escapes@^7.24.6":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.6.tgz#c8ddca8fd5bacece837a4e27bd3b7ed64580d1a8"
@ -940,7 +971,7 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.24.6":
"@babel/template@^7.23.9", "@babel/template@^7.24.6":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.6.tgz#048c347b2787a6072b24c723664c8d02b67a44f9"
integrity sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==
@ -949,7 +980,7 @@
"@babel/parser" "^7.24.6"
"@babel/types" "^7.24.6"
"@babel/traverse@^7.24.6":
"@babel/traverse@^7.23.9", "@babel/traverse@^7.24.6":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.6.tgz#0941ec50cdeaeacad0911eb67ae227a4f8424edc"
integrity sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==
@ -965,7 +996,7 @@
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.24.6", "@babel/types@^7.4.4":
"@babel/types@^7.22.15", "@babel/types@^7.23.9", "@babel/types@^7.24.6", "@babel/types@^7.4.4":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.6.tgz#ba4e1f59870c10dc2fa95a274ac4feec23b21912"
integrity sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==
@ -1975,11 +2006,53 @@
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
"@vitejs/plugin-vue-jsx@^3.0.1":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.1.0.tgz#9953fd9456539e1f0f253bf0fcd1289e66c67cd1"
integrity sha512-w9M6F3LSEU5kszVb9An2/MmXNxocAnUb3WhRr8bHlimhDrXNt6n6D2nJQR3UXpGlZHh/EsgouOHCsM8V3Ln+WA==
dependencies:
"@babel/core" "^7.23.3"
"@babel/plugin-transform-typescript" "^7.23.3"
"@vue/babel-plugin-jsx" "^1.1.5"
"@vitejs/plugin-vue@^4.0.0", "@vitejs/plugin-vue@^4.2.3":
version "4.6.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz#057d2ded94c4e71b94e9814f92dcd9306317aa46"
integrity sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==
"@vue/babel-helper-vue-transform-on@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.2.tgz#7f1f817a4f00ad531651a8d1d22e22d9e42807ef"
integrity sha512-nOttamHUR3YzdEqdM/XXDyCSdxMA9VizUKoroLX6yTyRtggzQMHXcmwh8a7ZErcJttIBIc9s68a1B8GZ+Dmvsw==
"@vue/babel-plugin-jsx@^1.1.5":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.2.tgz#eb426fb4660aa510bb8d188ff0ec140405a97d8a"
integrity sha512-nYTkZUVTu4nhP199UoORePsql0l+wj7v/oyQjtThUVhJl1U+6qHuoVhIvR3bf7eVKjbCK+Cs2AWd7mi9Mpz9rA==
dependencies:
"@babel/helper-module-imports" "~7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-jsx" "^7.23.3"
"@babel/template" "^7.23.9"
"@babel/traverse" "^7.23.9"
"@babel/types" "^7.23.9"
"@vue/babel-helper-vue-transform-on" "1.2.2"
"@vue/babel-plugin-resolve-type" "1.2.2"
camelcase "^6.3.0"
html-tags "^3.3.1"
svg-tags "^1.0.0"
"@vue/babel-plugin-resolve-type@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.2.tgz#66844898561da6449e0f4a261b0c875118e0707b"
integrity sha512-EntyroPwNg5IPVdUJupqs0CFzuf6lUrVvCspmv2J1FITLeGnUCuoGNNk78dgCusxEiYj6RMkTJflGSxk5aIC4A==
dependencies:
"@babel/code-frame" "^7.23.5"
"@babel/helper-module-imports" "~7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/parser" "^7.23.9"
"@vue/compiler-sfc" "^3.4.15"
"@vue/compiler-core@3.4.21":
version "3.4.21"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.21.tgz#868b7085378fc24e58c9aed14c8d62110a62be1a"
@ -1991,6 +2064,17 @@
estree-walker "^2.0.2"
source-map-js "^1.0.2"
"@vue/compiler-core@3.4.27":
version "3.4.27"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.27.tgz#e69060f4b61429fe57976aa5872cfa21389e4d91"
integrity sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==
dependencies:
"@babel/parser" "^7.24.4"
"@vue/shared" "3.4.27"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-dom@3.4.21":
version "3.4.21"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz#0077c355e2008207283a5a87d510330d22546803"
@ -1999,6 +2083,14 @@
"@vue/compiler-core" "3.4.21"
"@vue/shared" "3.4.21"
"@vue/compiler-dom@3.4.27":
version "3.4.27"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz#d51d35f40d00ce235d7afc6ad8b09dfd92b1cc1c"
integrity sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==
dependencies:
"@vue/compiler-core" "3.4.27"
"@vue/shared" "3.4.27"
"@vue/compiler-sfc@3.4.21":
version "3.4.21"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz#4af920dc31ab99e1ff5d152b5fe0ad12181145b2"
@ -2014,6 +2106,21 @@
postcss "^8.4.35"
source-map-js "^1.0.2"
"@vue/compiler-sfc@^3.4.15":
version "3.4.27"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz#399cac1b75c6737bf5440dc9cf3c385bb2959701"
integrity sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==
dependencies:
"@babel/parser" "^7.24.4"
"@vue/compiler-core" "3.4.27"
"@vue/compiler-dom" "3.4.27"
"@vue/compiler-ssr" "3.4.27"
"@vue/shared" "3.4.27"
estree-walker "^2.0.2"
magic-string "^0.30.10"
postcss "^8.4.38"
source-map-js "^1.2.0"
"@vue/compiler-ssr@3.4.21":
version "3.4.21"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz#b84ae64fb9c265df21fc67f7624587673d324fef"
@ -2022,6 +2129,14 @@
"@vue/compiler-dom" "3.4.21"
"@vue/shared" "3.4.21"
"@vue/compiler-ssr@3.4.27":
version "3.4.27"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz#2a8ecfef1cf448b09be633901a9c020360472e3d"
integrity sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==
dependencies:
"@vue/compiler-dom" "3.4.27"
"@vue/shared" "3.4.27"
"@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.5.1":
version "6.6.1"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.1.tgz#7c14346383751d9f6ad4bea0963245b30220ef83"
@ -2064,6 +2179,11 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.21.tgz#de526a9059d0a599f0b429af7037cd0c3ed7d5a1"
integrity sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==
"@vue/shared@3.4.27":
version "3.4.27"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.27.tgz#f05e3cd107d157354bb4ae7a7b5fc9cf73c63b50"
integrity sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==
"@vueuse/core@10.9.0", "@vueuse/core@^10.3.0", "@vueuse/core@^10.4.1", "@vueuse/core@^10.5.0":
version "10.9.0"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.9.0.tgz#7d779a95cf0189de176fee63cee4ba44b3c85d64"
@ -2372,6 +2492,11 @@ camelcase-css@^2.0.1:
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
camelcase@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
version "1.0.30001607"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001607.tgz#b91e8e033f6bca4e13d3d45388d87fa88931d9a5"
@ -3425,6 +3550,11 @@ html-encoding-sniffer@^3.0.0:
dependencies:
whatwg-encoding "^2.0.0"
html-tags@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
http-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
@ -3970,6 +4100,13 @@ magic-string@^0.25.0, magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.8"
magic-string@^0.30.10:
version "0.30.10"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e"
integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
magic-string@^0.30.7:
version "0.30.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.9.tgz#8927ae21bfdd856310e07a1bc8dd5e73cb6c251d"
@ -5251,6 +5388,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
svg-tags@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"