fix: added attendee, color, link in event modal and fixed existing bug
This commit is contained in:
parent
52d99ebf20
commit
8031964d3d
@ -25,8 +25,7 @@
|
||||
<EventModal
|
||||
v-if="showEventModal"
|
||||
v-model="showEventModal"
|
||||
v-model:events="events"
|
||||
:event="event"
|
||||
:event="activeEvent"
|
||||
:doctype="doctype"
|
||||
:docname="doc?.name"
|
||||
/>
|
||||
@ -36,6 +35,7 @@ import TaskModal from '@/components/Modals/TaskModal.vue'
|
||||
import NoteModal from '@/components/Modals/NoteModal.vue'
|
||||
import CallLogModal from '@/components/Modals/CallLogModal.vue'
|
||||
import EventModal from '@/components/Modals/EventModal.vue'
|
||||
import { showEventModal, activeEvent } from '@/composables/event'
|
||||
import { call } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@ -46,22 +46,11 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const activities = defineModel()
|
||||
const events = defineModel('events')
|
||||
|
||||
const showEventModal = ref(false)
|
||||
const event = ref({})
|
||||
|
||||
// Event
|
||||
function showEvent(e) {
|
||||
event.value = e || {
|
||||
subject: '',
|
||||
description: '',
|
||||
starts_on: '',
|
||||
ends_on: '',
|
||||
all_day: false,
|
||||
event_type: 'Public',
|
||||
color: 'green',
|
||||
}
|
||||
showEventModal.value = true
|
||||
activeEvent.value = e
|
||||
}
|
||||
|
||||
// Tasks
|
||||
|
||||
@ -58,31 +58,25 @@
|
||||
<div
|
||||
class="flex justify-between gap-2 items-center text-ink-gray-6"
|
||||
>
|
||||
<div>{{ formattedDateTime(event) }}</div>
|
||||
<div>{{ formattedDate(event) }}</div>
|
||||
<div>
|
||||
{{
|
||||
startEndTime(event.starts_on, event.ends_on, event.all_day)
|
||||
}}
|
||||
</div>
|
||||
<div>{{ startDate(event.starts_on) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EventModal
|
||||
v-if="showEventModal"
|
||||
v-model="showEventModal"
|
||||
v-model:events="eventsResource"
|
||||
:event="event"
|
||||
:doctype="doctype"
|
||||
:docname="docname"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
|
||||
import EventModal from '@/components/Modals/EventModal.vue'
|
||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { useEvent, showEventModal, activeEvent } from '@/composables/event'
|
||||
import { formatDate, timeAgo } from '@/utils'
|
||||
import { Tooltip, Avatar, dayjs, createListResource } from 'frappe-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Tooltip, Avatar } from 'frappe-ui'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: {
|
||||
@ -95,103 +89,13 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const { getUser } = usersStore()
|
||||
|
||||
const eventsResource = createListResource({
|
||||
doctype: 'Event',
|
||||
cache: ['calendar', props.docname],
|
||||
fields: [
|
||||
'name',
|
||||
'status',
|
||||
'subject',
|
||||
'description',
|
||||
'starts_on',
|
||||
'ends_on',
|
||||
'all_day',
|
||||
'event_type',
|
||||
'color',
|
||||
'owner',
|
||||
'reference_doctype',
|
||||
'reference_docname',
|
||||
'creation',
|
||||
],
|
||||
filters: {
|
||||
reference_doctype: props.doctype,
|
||||
reference_docname: props.docname,
|
||||
},
|
||||
auto: true,
|
||||
orderBy: 'creation desc',
|
||||
onSuccess: (d) => {
|
||||
console.log(d)
|
||||
},
|
||||
})
|
||||
|
||||
const eventParticipants = createListResource({
|
||||
doctype: 'Event Participants',
|
||||
cache: ['Event Participants', props.docname],
|
||||
fields: ['*'],
|
||||
parent: 'Event',
|
||||
})
|
||||
|
||||
const events = computed(() => {
|
||||
if (!eventsResource.data) return []
|
||||
|
||||
if (!eventParticipants.data?.length) {
|
||||
eventParticipants.update({
|
||||
filters: {
|
||||
parenttype: 'Event',
|
||||
parentfield: 'event_participants',
|
||||
parent: ['in', eventsResource.data.map((e) => e.name)],
|
||||
},
|
||||
})
|
||||
!eventParticipants.list.loading && eventParticipants.reload()
|
||||
} else {
|
||||
eventsResource.data.forEach((event) => {
|
||||
if (typeof event.owner !== 'object') {
|
||||
event.owner = {
|
||||
label: getUser(event.owner).full_name,
|
||||
image: getUser(event.owner).user_image,
|
||||
name: event.owner,
|
||||
}
|
||||
}
|
||||
|
||||
event.participants = [
|
||||
event.owner,
|
||||
...eventParticipants.data
|
||||
.filter((participant) => participant.parent === event.name)
|
||||
.map((participant) => ({
|
||||
label: getUser(participant.email).full_name || participant.email,
|
||||
image: getUser(participant.email).user_image || '',
|
||||
name: participant.email,
|
||||
})),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
return eventsResource.data
|
||||
})
|
||||
|
||||
const formattedDateTime = (e) => {
|
||||
const start = dayjs(e.starts_on)
|
||||
const end = dayjs(e.ends_on)
|
||||
|
||||
if (e.all_day) {
|
||||
return __('All day')
|
||||
}
|
||||
|
||||
return `${start.format('h:mm a')} - ${end.format('h:mm a')}`
|
||||
}
|
||||
|
||||
const formattedDate = (e) => {
|
||||
const start = dayjs(e.starts_on)
|
||||
return start.format('ddd, D MMM YYYY')
|
||||
}
|
||||
|
||||
const showEventModal = ref(false)
|
||||
const event = ref(null)
|
||||
|
||||
function showEvent(e) {
|
||||
showEventModal.value = true
|
||||
event.value = e
|
||||
activeEvent.value = e
|
||||
}
|
||||
|
||||
const { events, startEndTime, startDate } = useEvent(
|
||||
props.doctype,
|
||||
props.docname,
|
||||
)
|
||||
</script>
|
||||
|
||||
@ -42,15 +42,29 @@
|
||||
<div class="text-base text-ink-gray-7 w-3/12">
|
||||
{{ __('Title') }}
|
||||
</div>
|
||||
<TextInput
|
||||
ref="title"
|
||||
class="w-9/12"
|
||||
size="md"
|
||||
v-model="_event.title"
|
||||
:placeholder="__('Call with John Doe')"
|
||||
variant="outline"
|
||||
required
|
||||
/>
|
||||
<div class="flex gap-1 w-9/12">
|
||||
<Dropdown class="" :options="colors">
|
||||
<div
|
||||
class="flex items-center justify-center size-7 shrink-0 border border-outline-gray-2 bg-surface-white hover:border-outline-gray-3 hover:shadow-sm rounded cursor-pointer"
|
||||
>
|
||||
<div
|
||||
class="size-2.5 rounded-full cursor-pointer"
|
||||
:style="{
|
||||
backgroundColor: _event.color || '#30A66D',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<TextInput
|
||||
class="w-full"
|
||||
ref="title"
|
||||
size="sm"
|
||||
v-model="_event.title"
|
||||
:placeholder="__('Call with John Doe')"
|
||||
variant="outline"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="text-base text-ink-gray-7 w-3/12">
|
||||
@ -83,7 +97,7 @@
|
||||
</DatePicker>
|
||||
<TimePicker
|
||||
v-if="!_event.isFullDay"
|
||||
class="max-w-[105px]"
|
||||
class="max-w-[112px]"
|
||||
variant="outline"
|
||||
:modelValue="_event.fromTime"
|
||||
:placeholder="__('Start Time')"
|
||||
@ -91,7 +105,7 @@
|
||||
/>
|
||||
<TimePicker
|
||||
v-if="!_event.isFullDay"
|
||||
class="max-w-[105px]"
|
||||
class="max-w-[112px]"
|
||||
variant="outline"
|
||||
:modelValue="_event.toTime"
|
||||
:options="toOptions"
|
||||
@ -101,6 +115,50 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="text-base text-ink-gray-7 w-3/12">
|
||||
{{ __('Link') }}
|
||||
</div>
|
||||
<div class="flex gap-2 w-9/12">
|
||||
<FormControl
|
||||
:class="_event.referenceDoctype ? 'w-20' : 'w-full'"
|
||||
type="select"
|
||||
:options="linkDoctypeOptions"
|
||||
v-model="_event.referenceDoctype"
|
||||
variant="outline"
|
||||
:placeholder="__('Add Lead or Deal')"
|
||||
@change="() => (_event.referenceDocname = '')"
|
||||
/>
|
||||
<Link
|
||||
v-if="_event.referenceDoctype"
|
||||
class="w-full"
|
||||
v-model="_event.referenceDocname"
|
||||
:doctype="_event.referenceDoctype"
|
||||
variant="outline"
|
||||
:placeholder="
|
||||
__('Select {0}', [
|
||||
_event.referenceDoctype == 'CRM Lead'
|
||||
? __('Lead')
|
||||
: __('Deal'),
|
||||
])
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<div class="text-base text-ink-gray-7 mt-1.5 w-3/12">
|
||||
{{ __('Attendees') }}
|
||||
</div>
|
||||
<div class="w-9/12">
|
||||
<Attendee
|
||||
v-model="peoples"
|
||||
:validate="validateEmail"
|
||||
:error-message="
|
||||
(value) => __('{0} is an invalid email address', [value])
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="mt-2 text-base text-ink-gray-7 w-3/12">
|
||||
{{ __('Description') }}
|
||||
@ -119,7 +177,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<div v-if="eventsResource" class="flex gap-2 justify-end">
|
||||
<Button :label="__('Cancel')" @click="show = false" />
|
||||
<Button
|
||||
variant="solid"
|
||||
@ -131,7 +189,9 @@
|
||||
: __('Create')
|
||||
"
|
||||
:loading="
|
||||
mode === 'edit' ? events.setValue.loading : events.insert.loading
|
||||
mode === 'edit'
|
||||
? eventsResource.setValue.loading
|
||||
: eventsResource.insert.loading
|
||||
"
|
||||
@click="update"
|
||||
/>
|
||||
@ -140,6 +200,8 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import Attendee from '@/components/Calendar/Attendee.vue'
|
||||
import {
|
||||
Switch,
|
||||
TextEditor,
|
||||
@ -148,9 +210,15 @@ import {
|
||||
DatePicker,
|
||||
TimePicker,
|
||||
dayjs,
|
||||
Dropdown,
|
||||
FormControl,
|
||||
} from 'frappe-ui'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import { validateEmail } from '@/utils'
|
||||
import { allTimeSlots } from '@/components/Calendar/utils'
|
||||
import { useEvent } from '@/composables/event'
|
||||
import { CalendarColorMap as colorMap } from 'frappe-ui'
|
||||
import { onMounted, ref, computed, h } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
event: {
|
||||
@ -170,7 +238,8 @@ const props = defineProps({
|
||||
const { $dialog } = globalStore()
|
||||
|
||||
const show = defineModel()
|
||||
const events = defineModel('events')
|
||||
|
||||
const { eventsResource } = useEvent(props.doctype, props.docname)
|
||||
|
||||
const title = ref(null)
|
||||
const error = ref(null)
|
||||
@ -192,6 +261,29 @@ const _event = ref({
|
||||
isFullDay: false,
|
||||
eventType: 'Public',
|
||||
color: 'green',
|
||||
referenceDoctype: '',
|
||||
referenceDocname: '',
|
||||
event_participants: [],
|
||||
})
|
||||
|
||||
const peoples = computed({
|
||||
get() {
|
||||
return _event.value.event_participants || []
|
||||
},
|
||||
set(list) {
|
||||
const seen = new Set()
|
||||
const out = []
|
||||
for (const a of list || []) {
|
||||
if (!a?.email || seen.has(a.email)) continue
|
||||
seen.add(a.email)
|
||||
out.push({
|
||||
email: a.email,
|
||||
reference_doctype: a.reference_doctype || 'Contact',
|
||||
reference_docname: a.reference_docname || '',
|
||||
})
|
||||
}
|
||||
_event.value.event_participants = out
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
@ -215,6 +307,9 @@ onMounted(() => {
|
||||
isFullDay: props.event.all_day,
|
||||
eventType: props.event.event_type,
|
||||
color: props.event.color,
|
||||
referenceDoctype: props.event.reference_doctype,
|
||||
referenceDocname: props.event.reference_docname,
|
||||
event_participants: props.event.event_participants || [],
|
||||
}
|
||||
|
||||
setTimeout(() => title.value?.el?.focus(), 100)
|
||||
@ -235,24 +330,35 @@ function updateTime(t, fromTime = false) {
|
||||
if (!_event.value.toTime) {
|
||||
const hour = parseInt(t.split(':')[0])
|
||||
const minute = parseInt(t.split(':')[1])
|
||||
_event.value.toTime = `${hour + 1}:${minute}`
|
||||
let nh = hour + 1
|
||||
let nm = minute
|
||||
if (nh >= 24) {
|
||||
nh = 23
|
||||
nm = 59
|
||||
}
|
||||
_event.value.toTime = `${String(nh).padStart(2, '0')}:${String(nm).padStart(2, '0')}`
|
||||
}
|
||||
} else {
|
||||
_event.value.toTime = t
|
||||
}
|
||||
|
||||
validateFromToTime(oldTo)
|
||||
}
|
||||
|
||||
function validateFromToTime(oldTo) {
|
||||
if (_event.value.isFullDay) return true
|
||||
if (_event.value.toTime && _event.value.fromTime) {
|
||||
const diff = dayjs(_event.value.toDate + ' ' + _event.value.toTime).diff(
|
||||
const diff = dayjs(_event.value.fromDate + ' ' + _event.value.toTime).diff(
|
||||
dayjs(_event.value.fromDate + ' ' + _event.value.fromTime),
|
||||
'minute',
|
||||
)
|
||||
|
||||
if (diff <= 0) {
|
||||
_event.value.toTime = oldTo
|
||||
error.value = __('End time should be after start time')
|
||||
return
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function update() {
|
||||
@ -263,11 +369,17 @@ function update() {
|
||||
return
|
||||
}
|
||||
|
||||
_event.value.id ? updateEvent() : createEvent()
|
||||
validateFromToTime(_event.value.toTime)
|
||||
|
||||
if (_event.value.id && _event.value.id !== 'duplicate') {
|
||||
updateEvent()
|
||||
} else {
|
||||
createEvent()
|
||||
}
|
||||
}
|
||||
|
||||
function createEvent() {
|
||||
events.value.insert.submit(
|
||||
eventsResource.insert.submit(
|
||||
{
|
||||
subject: _event.value.title,
|
||||
description: _event.value.description,
|
||||
@ -278,10 +390,13 @@ function createEvent() {
|
||||
color: _event.value.color,
|
||||
reference_doctype: props.doctype,
|
||||
reference_docname: props.docname,
|
||||
reference_doctype: _event.value.referenceDoctype || props.doctype,
|
||||
reference_docname: _event.value.referenceDocname || props.docname,
|
||||
event_participants: _event.value.event_participants,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await events.value.reload()
|
||||
await eventsResource.reload()
|
||||
show.value = false
|
||||
},
|
||||
},
|
||||
@ -294,7 +409,7 @@ function updateEvent() {
|
||||
return
|
||||
}
|
||||
|
||||
events.value.setValue.submit(
|
||||
eventsResource.setValue.submit(
|
||||
{
|
||||
name: _event.value.id,
|
||||
subject: _event.value.title,
|
||||
@ -306,10 +421,13 @@ function updateEvent() {
|
||||
color: _event.value.color,
|
||||
reference_doctype: props.doctype,
|
||||
reference_docname: props.docname,
|
||||
reference_doctype: _event.value.referenceDoctype || props.doctype,
|
||||
reference_docname: _event.value.referenceDocname || props.docname,
|
||||
event_participants: _event.value.event_participants,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await events.value.reload()
|
||||
await eventsResource.reload()
|
||||
show.value = false
|
||||
},
|
||||
},
|
||||
@ -336,9 +454,9 @@ function deleteEvent() {
|
||||
variant: 'solid',
|
||||
theme: 'red',
|
||||
onClick: (close) => {
|
||||
events.value.delete.submit(_event.value.id, {
|
||||
eventsResource.delete.submit(_event.value.id, {
|
||||
onSuccess: async () => {
|
||||
await events.value.reload()
|
||||
await eventsResource.reload()
|
||||
show.value = false
|
||||
close()
|
||||
},
|
||||
@ -386,20 +504,19 @@ const toOptions = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
function allTimeSlots() {
|
||||
const out = []
|
||||
for (let h = 0; h < 24; h++) {
|
||||
for (const m of [0, 15, 30, 45]) {
|
||||
const hh = String(h).padStart(2, '0')
|
||||
const mm = String(m).padStart(2, '0')
|
||||
const ampm = h >= 12 ? 'pm' : 'am'
|
||||
const hour12 = h % 12 === 0 ? 12 : h % 12
|
||||
out.push({
|
||||
value: `${hh}:${mm}`,
|
||||
label: `${hour12}:${mm} ${ampm}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
const linkDoctypeOptions = [
|
||||
{ label: '', value: '' },
|
||||
{ label: __('Lead'), value: 'CRM Lead' },
|
||||
{ label: __('Deal'), value: 'CRM Deal' },
|
||||
]
|
||||
|
||||
const colors = Object.keys(colorMap).map((c) => ({
|
||||
label: c.charAt(0).toUpperCase() + c.slice(1),
|
||||
value: colorMap[c].color,
|
||||
icon: h('div', {
|
||||
class: '!size-2.5 rounded-full',
|
||||
style: { backgroundColor: colorMap[c].color },
|
||||
}),
|
||||
onClick: () => (_event.value.color = colorMap[c].color),
|
||||
}))
|
||||
</script>
|
||||
|
||||
138
frontend/src/composables/event.js
Normal file
138
frontend/src/composables/event.js
Normal file
@ -0,0 +1,138 @@
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { dayjs, createListResource } from 'frappe-ui'
|
||||
import { sameArrayContents } from '@/utils'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export const showEventModal = ref(false)
|
||||
export const activeEvent = ref(null)
|
||||
|
||||
export function useEvent(doctype, docname) {
|
||||
const { getUser } = usersStore()
|
||||
|
||||
const eventsResource = createListResource({
|
||||
doctype: 'Event',
|
||||
cache: ['calendar', docname],
|
||||
fields: [
|
||||
'name',
|
||||
'status',
|
||||
'subject',
|
||||
'description',
|
||||
'starts_on',
|
||||
'ends_on',
|
||||
'all_day',
|
||||
'event_type',
|
||||
'color',
|
||||
'owner',
|
||||
'reference_doctype',
|
||||
'reference_docname',
|
||||
'creation',
|
||||
],
|
||||
filters: {
|
||||
reference_doctype: doctype,
|
||||
reference_docname: docname,
|
||||
},
|
||||
auto: true,
|
||||
orderBy: 'creation desc',
|
||||
onSuccess: (d) => {
|
||||
console.log(d)
|
||||
},
|
||||
})
|
||||
|
||||
const eventParticipantsResource = createListResource({
|
||||
doctype: 'Event Participants',
|
||||
fields: ['*'],
|
||||
parent: 'Event',
|
||||
})
|
||||
|
||||
const events = computed(() => {
|
||||
if (!eventsResource.data) return []
|
||||
const eventNames = eventsResource.data.map((e) => e.name)
|
||||
if (
|
||||
!eventParticipantsResource.data?.length ||
|
||||
eventsParticipantIsUpdated(eventNames)
|
||||
) {
|
||||
eventParticipantsResource.update({
|
||||
filters: {
|
||||
parenttype: 'Event',
|
||||
parentfield: 'event_participants',
|
||||
parent: ['in', eventNames],
|
||||
},
|
||||
})
|
||||
!eventParticipantsResource.list.loading &&
|
||||
eventParticipantsResource.reload()
|
||||
} else {
|
||||
eventsResource.data.forEach((event) => {
|
||||
if (typeof event.owner !== 'object') {
|
||||
event.owner = {
|
||||
label: getUser(event.owner).full_name,
|
||||
image: getUser(event.owner).user_image,
|
||||
name: event.owner,
|
||||
}
|
||||
}
|
||||
|
||||
event.event_participants = [
|
||||
...eventParticipantsResource.data.filter(
|
||||
(participant) => participant.parent === event.name,
|
||||
),
|
||||
]
|
||||
|
||||
event.participants = [
|
||||
event.owner,
|
||||
...eventParticipantsResource.data
|
||||
.filter((participant) => participant.parent === event.name)
|
||||
.map((participant) => ({
|
||||
label: getUser(participant.email).full_name || participant.email,
|
||||
image: getUser(participant.email).user_image || '',
|
||||
name: participant.email,
|
||||
})),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
return eventsResource.data
|
||||
})
|
||||
|
||||
function eventsParticipantIsUpdated(eventNames) {
|
||||
const parentFilter = eventParticipantsResource.filters?.parent?.[1]
|
||||
|
||||
if (eventNames.length && !sameArrayContents(parentFilter, eventNames))
|
||||
return true
|
||||
|
||||
let d = eventsResource.setValue.data
|
||||
if (!d) return false
|
||||
|
||||
let newParticipants = d.event_participants.map((p) => p.name)
|
||||
let oldParticipants = eventParticipantsResource.data
|
||||
.filter((p) => p.parent === d.name)
|
||||
.map((p) => p.name)
|
||||
|
||||
return !sameArrayContents(newParticipants, oldParticipants)
|
||||
}
|
||||
|
||||
const startEndTime = (
|
||||
startTime,
|
||||
endTime,
|
||||
isFullDay = false,
|
||||
format = 'h:mm a',
|
||||
) => {
|
||||
const start = dayjs(startTime)
|
||||
const end = dayjs(endTime)
|
||||
|
||||
if (isFullDay) return __('All day')
|
||||
|
||||
return `${start.format(format)} - ${end.format(format)}`
|
||||
}
|
||||
|
||||
const startDate = (startTime, format = 'ddd, D MMM YYYY') => {
|
||||
const start = dayjs(startTime)
|
||||
return start.format(format)
|
||||
}
|
||||
|
||||
return {
|
||||
eventsResource,
|
||||
eventParticipantsResource,
|
||||
events,
|
||||
startEndTime,
|
||||
startDate,
|
||||
}
|
||||
}
|
||||
@ -689,3 +689,33 @@ export function validateConditions(conditions) {
|
||||
|
||||
return conditions.length > 0
|
||||
}
|
||||
|
||||
// sameArrayContents: returns true if both arrays have exactly the same elements
|
||||
// (including duplicate counts) irrespective of order.
|
||||
// Non-arrays or arrays of different length return false.
|
||||
export function sameArrayContents(a, b) {
|
||||
if (a === b) return true
|
||||
if (!Array.isArray(a) || !Array.isArray(b)) return false
|
||||
if (a.length !== b.length) return false
|
||||
if (a.length === 0) return true
|
||||
const counts = new Map()
|
||||
for (const v of a) {
|
||||
counts.set(v, (counts.get(v) || 0) + 1)
|
||||
}
|
||||
for (const v of b) {
|
||||
const c = counts.get(v)
|
||||
if (!c) return false
|
||||
if (c === 1) counts.delete(v)
|
||||
else counts.set(v, c - 1)
|
||||
}
|
||||
return counts.size === 0
|
||||
}
|
||||
|
||||
// orderSensitiveEqual: returns true only if arrays are strictly equal index-wise
|
||||
export function orderSensitiveEqual(a, b) {
|
||||
if (a === b) return true
|
||||
if (!Array.isArray(a) || !Array.isArray(b)) return false
|
||||
if (a.length !== b.length) return false
|
||||
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
||||
return true
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user