Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
1a8fee7
feat: init calendar
krantheman Mar 9, 2026
7c9e1b1
feat: show events
krantheman Mar 10, 2026
dc4b848
feat: add events
krantheman Mar 12, 2026
668b58a
fix: calculation of duration
krantheman Mar 12, 2026
bd5afe8
feat: show calendars
krantheman Mar 12, 2026
2523c3c
feat: delete events
krantheman Mar 12, 2026
42f96d1
feat: show mail contacts directly instead of dialog
krantheman Mar 13, 2026
4c78821
refactor: collapse steps and add location
krantheman Mar 13, 2026
23ed4f6
feat: add repeat settings
krantheman Mar 13, 2026
5069e86
feat: add view event
krantheman Mar 16, 2026
80d2c43
feat: add availability and privacy
krantheman Mar 16, 2026
e423eec
feat: show recurring events
krantheman Mar 17, 2026
c562b94
feat: add complex condition for repeat
krantheman Mar 17, 2026
f7f64a6
feat: add scheduling messages as a dialog
krantheman Mar 19, 2026
d59d923
Merge branch 'develop' into feat-calendar
krantheman Mar 19, 2026
ca70732
feat: add edit event
krantheman Mar 19, 2026
70e724e
Merge branch 'develop' into feat-calendar
krantheman Mar 19, 2026
8b75e5e
Merge branch 'develop' into feat-calendar
krantheman Mar 23, 2026
5a5b324
fix: all day events start
krantheman Mar 23, 2026
cca6427
feat: multiple
krantheman Mar 23, 2026
80b71e2
feat: show date time in event popover
krantheman Mar 24, 2026
98ffb17
feat: show repeat message in event popover
krantheman Mar 24, 2026
f35cc42
refactor: extracting formatting helpers to format.ts
krantheman Mar 24, 2026
b205cd4
feat(EventPopoverContent): show description and location
krantheman Mar 24, 2026
fd1dfec
refactor(EventPopoverContent): just a single more menu for actions
krantheman Mar 24, 2026
db2ae41
feat(EventPopoverContent): make description expandable
krantheman Mar 24, 2026
1ad5ef9
Merge branch 'develop' into feat-calendar
krantheman Mar 25, 2026
592630a
fix: fetching of recurrence rule
krantheman Mar 25, 2026
740524a
Merge branch 'develop' into feat-calendar
krantheman Mar 25, 2026
e37b652
feat: add RSVP
krantheman Mar 25, 2026
7a97360
feat: add RSVP for recurring events
krantheman Mar 25, 2026
cb39070
feat: add support for edit instance
krantheman Mar 26, 2026
48aa79f
Merge branch 'develop' into feat-calendar
krantheman Mar 26, 2026
aa4c012
refactor: participants list
krantheman Mar 26, 2026
88ab0b0
feat(EditEventModal): add rsvp
krantheman Mar 26, 2026
5a21171
refactor: extract EventParticipantList
krantheman Mar 26, 2026
94f4c62
feat(EventPopoverContent): show participants
krantheman Mar 27, 2026
010513b
feat: add support for multiple locations
krantheman Mar 27, 2026
c991de4
feat: add alert inputs
krantheman Mar 27, 2026
b78f4c5
feat: show and set alerts
krantheman Mar 27, 2026
008c6e2
fix: extraction of unit for alerts
krantheman Mar 27, 2026
16614bf
refactor: extract EventAlertsInput
krantheman Mar 27, 2026
691dad3
chore: rename EventModal and EventAlertList
krantheman Mar 27, 2026
f9a2763
fix: edit instance and show recurring modal
krantheman Mar 30, 2026
8613fb2
refactor: clean EventModal
krantheman Mar 30, 2026
4a2f1ce
refactor(EventRepeatSettingsModal): allow editing of current rule
krantheman Mar 30, 2026
9f8e78a
refactor: polish repeat and fix number parsing
krantheman Mar 30, 2026
2c37aab
feat: set start time for week and day view
krantheman Mar 30, 2026
93fb7df
fix: end date display
krantheman Mar 30, 2026
25c93fc
feat: add query params for view
krantheman Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ declare module 'vue' {
AttachmentCapsule: typeof import('./src/components/AttachmentCapsule.vue')['default']
AttachmentViewer: typeof import('./src/components/AttachmentViewer.vue')['default']
AudioIcon: typeof import('./src/components/Icons/AudioIcon.vue')['default']
CalendarLogo: typeof import('./src/components/Icons/CalendarLogo.vue')['default']
CalendarSidebar: typeof import('./src/components/CalendarSidebar.vue')['default']
ChangePasswordModal: typeof import('./src/components/Modals/ChangePasswordModal.vue')['default']
ComposeMailEditor: typeof import('./src/components/ComposeMailEditor.vue')['default']
ComposeMailToolbar: typeof import('./src/components/ComposeMailToolbar.vue')['default']
Expand All @@ -50,6 +52,11 @@ declare module 'vue' {
EditSignatureModal: typeof import('./src/components/Modals/EditSignatureModal.vue')['default']
EmailContent: typeof import('./src/components/EmailContent.vue')['default']
EmojiPicker: typeof import('./src/components/EmojiPicker.vue')['default']
EventAlertList: typeof import('./src/components/EventAlertList.vue')['default']
EventModal: typeof import('./src/components/Modals/EventModal.vue')['default']
EventParticipantList: typeof import('./src/components/EventParticipantList.vue')['default']
EventPopoverContent: typeof import('./src/components/EventPopoverContent.vue')['default']
EventRepeatSettingsModal: typeof import('./src/components/Modals/EventRepeatSettingsModal.vue')['default']
ExportSettings: typeof import('./src/components/Settings/ExportSettings.vue')['default']
FrappeLogo: typeof import('./src/components/Icons/FrappeLogo.vue')['default']
HeaderActions: typeof import('./src/components/HeaderActions.vue')['default']
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/AppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ import { useRoute, useRouter } from 'vue-router'
import { useStorage } from '@vueuse/core'
import { Button, Dropdown, Sidebar, SidebarItem, createResource } from 'frappe-ui'

import { toTitleCase } from '@/utils'
import { useScreenSize, useSidebar } from '@/utils/composables'
import { toTitleCase } from '@/utils/format'
import { sessionStore } from '@/stores/session'
import { userStore } from '@/stores/user'
import MailLogo from '@/components/Icons/MailLogo.vue'
Expand Down
81 changes: 81 additions & 0 deletions frontend/src/components/CalendarSidebar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script setup lang="ts">
import { computed, h, inject } from 'vue'
import { Eye, EyeOff, LayoutGrid, LogOut } from 'lucide-vue-next'
import { Sidebar, createResource } from 'frappe-ui'

import { toTitleCase } from '@/utils/format'
import { sessionStore } from '@/stores/session'
import CalendarLogo from '@/components/Icons/CalendarLogo.vue'

const { calendars, visibleCalendars } = defineProps<{
calendars: any[]
visibleCalendars: string[]
}>()

const emit = defineEmits(['update:visibleCalendars'])

const { branding, logout } = sessionStore()

const user = inject('$user')

const title = computed(() =>
branding.data?.brand_name && branding.data?.brand_name != 'Frappe'
? branding.data.brand_name
: 'Calendar',
)

const apps = createResource({ url: 'mail.api.get_apps', cache: 'otherApps', auto: true })

const menuItems = computed(() => [
{
icon: LayoutGrid,
label: __('Apps'),
submenu: apps.data?.map?.((app) => ({
label: app.title,
icon: app.logo,
component: h(
'a',
{
class: 'flex items-center gap-2 p-1.5 rounded hover:bg-surface-gray-2',
href: app.route,
},
[
h('img', { src: app.logo, class: 'size-6' }),
h('span', { class: 'max-w-18 text-sm w-full truncate' }, app.title),
],
),
})),
condition: () => user.data.is_system_manager,
},
{
icon: LogOut,
label: __('Log Out'),
onClick: logout.submit,
},
])

const sidebarItems = computed(() => [
{
label: __('Calendars'),
items:
calendars.map((calendar) => ({
label: calendar._name,
icon: visibleCalendars.includes(calendar.name) ? Eye : EyeOff,
onClick: () => emit('update:visibleCalendars', calendar.name),
})) || [],
},
])
</script>

<template>
<Sidebar
:header="{
title,
subtitle: toTitleCase(user.data.full_name),
menuItems,
logo: branding.data?.brand_html || CalendarLogo,
}"
:sections="sidebarItems"
:disable-collapse="true"
/>
</template>
2 changes: 1 addition & 1 deletion frontend/src/components/ComposeMailEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -268,14 +268,14 @@ import {

import { getAttachmentUrl } from '@/resources'
import {
formatBytes,
isOverlayPresent,
processInlineImages,
raiseToast,
randomString,
validateEmail,
} from '@/utils'
import { useScreenSize, useVisualViewport } from '@/utils/composables'
import { formatBytes } from '@/utils/format'
import { CustomParagraphExtension } from '@/utils/text-editor'
import { userStore } from '@/stores/user'
import ComposeMailToolbar from '@/components/ComposeMailToolbar.vue'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ const mailContacts = createResource({
}),
transform: (data) =>
data.map((option) => ({ label: option.full_name || option.email, value: option.email })),
auto: false,
})

const debouncedSearch = useDebounceFn((text: string) => mailContacts.reload(text), 300)
Expand Down
99 changes: 99 additions & 0 deletions frontend/src/components/EventAlertList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script setup lang="ts">
import { Button, FormControl } from 'frappe-ui'

const { alerts } = defineProps<{ alerts: any[] }>()

const emit = defineEmits(['update:alerts'])

const updateAlert = (i: number, field: string, value: any) => {
const updated = alerts.map((a, idx) => (idx === i ? { ...a, [field]: value } : a))
emit('update:alerts', updated)
}

const removeAlert = (i: number) => {
const updated = alerts.filter((_, idx) => idx !== i)
emit('update:alerts', updated)
}

const ALERT_ACTION_OPTIONS = [
{ label: __('Screen Pop-up'), value: 'Display' },
{ label: __('Email Notice'), value: 'Email' },
{ label: __('Sound Alert'), value: 'Audio' },
]

const UNIT_OPTIONS = [
{ label: __('Minutes'), value: 'minutes' },
{ label: __('Hours'), value: 'hours' },
{ label: __('Days'), value: 'days' },
{ label: __('Weeks'), value: 'weeks' },
]

const DIRECTION_OPTIONS = [
{ label: __('Before'), value: -1 },
{ label: __('After'), value: 1 },
]

const RELATIVE_TO_OPTIONS = [
{ label: __('Start'), value: 'Start' },
{ label: __('End'), value: 'End' },
]
</script>

<template>
<div v-for="(alert, i) in alerts" :key="i" class="flex space-x-2">
<FormControl
:model-value="alert.action"
:label="i === 0 ? (alerts.length > 1 ? __('Alerts') : __('Alert')) : ''"
type="select"
:options="ALERT_ACTION_OPTIONS"
class="w-40 shrink-0"
@update:model-value="updateAlert(i, 'action', $event)"
/>
<template v-if="alert.type === 'OffsetTrigger'">
<FormControl
:model-value="alert.number"
type="number"
class="mt-auto w-16 shrink-0"
@update:model-value="updateAlert(i, 'number', $event)"
/>
<FormControl
:model-value="alert.unit"
type="select"
:options="UNIT_OPTIONS"
class="mt-auto w-full"
@update:model-value="updateAlert(i, 'unit', $event)"
/>
<FormControl
:model-value="alert.direction"
type="select"
:options="DIRECTION_OPTIONS"
class="mt-auto w-full"
@update:model-value="updateAlert(i, 'direction', $event)"
/>
<FormControl
:model-value="alert.relative_to"
type="select"
:options="RELATIVE_TO_OPTIONS"
class="mt-auto w-full"
@update:model-value="updateAlert(i, 'relative_to', $event)"
/>
</template>
<template v-else>
<span class="text-ink-gray-8 mb-1.5 mt-auto text-base">{{ __('on') }}</span>
<FormControl
:model-value="alert.date"
type="date"
class="mt-auto w-full"
@update:model-value="updateAlert(i, 'date', $event)"
/>
<span class="text-ink-gray-8 mb-1.5 mt-auto text-base">{{ __('at') }}</span>
<FormControl
:model-value="alert.time"
type="time"
class="mt-auto w-full"
@update:model-value="updateAlert(i, 'time', $event)"
/>
</template>
<Button icon="x" class="mt-auto" @click="removeAlert(i)" />
</div>
</template>
72 changes: 72 additions & 0 deletions frontend/src/components/EventParticipantList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Check, Minus, X } from 'lucide-vue-next'
import { Avatar, Button } from 'frappe-ui'

import { extractNameFromEmail } from '@/utils/format'
import { userStore } from '@/stores/user'

const { participants, dontShowRemove } = defineProps<{
participants: any[]
dontShowRemove?: boolean
}>()

defineEmits(['removeParticipant'])

const { identities } = userStore()

const organizer = computed(() => participants.find((p) => p.isOrganizer)?.email)

const isUserOrganizer = computed(() =>
identities.data.some((id) => id.email === organizer.value?.replace('mailto:', '')),
)

const showRemoveParticipant = (participant: any) =>
!participant.isOrganizer && (isUserOrganizer.value || participant.isNew) && !dontShowRemove

const getParticipantStatusValues = (status: string) => {
if (status === 'ACCEPTED') return { icon: Check, class: 'bg-surface-green-1 text-ink-green-3' }
if (status === 'TENTATIVE') return { icon: Minus, class: 'bg-surface-gray-1 text-ink-gray-6' }
return { icon: X, class: 'bg-surface-red-1 text-ink-red-3' }
}
</script>
<template>
<div v-for="p in participants" :key="p.email">
<div class="flex items-center justify-between text-left">
<div class="flex items-center space-x-2">
<Avatar :image="p.user_image" :label="p._name || p.email" size="xl" />
<div class="flex flex-col space-y-0.5">
<div class="flex items-center space-x-1">
<span class="text-sm font-medium">
{{ extractNameFromEmail(p._name || p.email) }}
</span>
<span v-if="p.email === organizer" class="text-ink-gray-4 text-xs">
({{ __('Organizer') }})
</span>

<div
v-if="
p.participation_status && p.participation_status !== 'NEEDS-ACTION'
"
class="rounded-full p-px"
:class="getParticipantStatusValues(p.participation_status).class"
>
<component
:is="getParticipantStatusValues(p.participation_status).icon"
class="h-3 w-3"
/>
</div>
</div>
<span class="text-ink-gray-4 text-sm">{{ p.email }}</span>
</div>
</div>

<Button
v-if="showRemoveParticipant(p)"
variant="ghost"
icon="x"
@click="$emit('removeParticipant', p.email)"
/>
</div>
</div>
</template>
Loading
Loading