chore: standardize chrono provider

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-02-17 03:12:12 -05:00
parent cc4e467cef
commit f2f7bfc391
11 changed files with 282 additions and 224 deletions

View File

@@ -27,15 +27,15 @@
<v-list-subheader>{{ formatAgendaDate(date) }}</v-list-subheader>
<v-list-item
v-for="entity in dayEvents"
:key="entity.id"
:key="entity.identifier"
@click="$emit('event-click', entity)"
>
<template #prepend>
<v-icon :color="getEventColor(entity)">mdi-circle</v-icon>
</template>
<v-list-item-title>{{ entity.data?.label || 'Untitled' }}</v-list-item-title>
<v-list-item-title>{{ entity.properties?.label || 'Untitled' }}</v-list-item-title>
<v-list-item-subtitle>
{{ entity.data?.allDay ? 'All day' : `${entity.data?.startsOn ? formatTime(new Date(entity.data.startsOn)) : ''} - ${entity.data?.endsOn ? formatTime(new Date(entity.data.endsOn)) : ''}` }}
{{ entity.properties?.timeless ? 'All day' : `${entity.properties?.startsOn ? formatTime(new Date(entity.properties.startsOn)) : ''} - ${entity.properties?.endsOn ? formatTime(new Date(entity.properties.endsOn)) : ''}` }}
</v-list-item-subtitle>
</v-list-item>
</template>
@@ -132,17 +132,17 @@ const groupedEvents = computed(() => {
const { start, end } = dateRange.value;
const filtered = props.events.filter(e => {
if (!e.data?.startsOn) return false;
const eventStart = new Date(e.data.startsOn);
if (!e.properties?.startsOn) return false;
const eventStart = new Date(e.properties.startsOn);
return eventStart >= start && eventStart <= end;
});
const sorted = filtered.sort((a, b) =>
new Date(a.data.startsOn).getTime() - new Date(b.data.startsOn).getTime()
new Date(a.properties.startsOn).getTime() - new Date(b.properties.startsOn).getTime()
);
sorted.forEach(entity => {
const dateKey = new Date(entity.data.startsOn).toDateString();
const dateKey = new Date(entity.properties.startsOn).toDateString();
if (!grouped[dateKey]) {
grouped[dateKey] = [];
}
@@ -153,9 +153,9 @@ const groupedEvents = computed(() => {
});
function getEventColor(entity: any): string {
if (entity.data?.color) return entity.data.color;
const calendar = props.calendars.find(cal => cal.id === entity.in);
return calendar?.color || '#1976D2';
if (entity.properties?.color) return entity.properties.color;
const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
return calendar?.properties?.color || '#1976D2';
}
function formatTime(date: Date): string {

View File

@@ -61,7 +61,7 @@ const dialogIcon = computed(() => {
// Functions
const onOpen = async () => {
if (services.value.length === 0) {
services.value = await servicesStore.list()
services.value = Object.values(await servicesStore.list())
}
if (!props.collection) {
@@ -71,10 +71,10 @@ const onOpen = async () => {
// Clone the collection to avoid mutating the original
editingCollection.value = props.collection.clone()
if (props.collection.id !== null) {
if (props.mode === 'edit') {
// Edit mode - find the service
editingCollectionService.value = services.value.find(s =>
s.provider === props.collection!.provider && s.id === props.collection!.service
s.provider === props.collection!.provider && s.identifier === props.collection!.service
) || null
} else {
// Create mode - use first service that can create
@@ -102,7 +102,7 @@ const onColorSelect = (color: string | null, closeMenu = true) => {
if (!editingCollection.value) {
return
}
editingCollection.value.color = color
editingCollection.value.properties.color = color
if (closeMenu) {
colorMenuOpen.value = false
}
@@ -144,7 +144,7 @@ watch(() => props.modelValue, async (newValue) => {
label="Service"
:items="services.filter(s => s.capabilities?.CollectionCreate)"
item-title="label"
item-value="id"
item-value="identifier"
required
:rules="[(v: ServiceObject) => !!v || 'Service is required']"
/>
@@ -152,7 +152,7 @@ watch(() => props.modelValue, async (newValue) => {
<div v-if="mode === 'edit'" class="mb-4"><strong>Service:</strong> {{ editingCollection.service }}</div>
<v-text-field
v-model="editingCollection.label"
v-model="editingCollection.properties.label"
label="Label"
required
:rules="[(v: string) => !!v || 'Label is required']"
@@ -170,7 +170,7 @@ watch(() => props.modelValue, async (newValue) => {
icon
variant="text"
size="small"
:style="{ color: editingCollection.color || 'var(--v-theme-on-surface)' }"
:style="{ color: editingCollection.properties.color || 'var(--v-theme-on-surface)' }"
aria-label="Select color"
title="Select color"
>
@@ -203,11 +203,11 @@ watch(() => props.modelValue, async (newValue) => {
variant="flat"
size="small"
class="color-menu-body__presets--swatch"
:class="{ 'color-menu-body__presets--swatch--active': editingCollection.color === color }"
:class="{ 'color-menu-body__presets--swatch--active': editingCollection.properties.color === color }"
:style="{ backgroundColor: color }"
@click="onColorSelect(color)">
<v-icon
v-if="editingCollection.color === color"
v-if="editingCollection.properties.color === color"
icon="mdi-check"
size="x-small"
color="white"
@@ -218,7 +218,7 @@ watch(() => props.modelValue, async (newValue) => {
<div class="color-menu-body__picker">
<v-color-picker
v-model="editingCollection.color"
v-model="editingCollection.properties.color"
mode="hex"
hide-canvas
width="100%"
@@ -232,7 +232,7 @@ watch(() => props.modelValue, async (newValue) => {
</v-text-field>
<v-textarea
v-model="editingCollection.description"
v-model="editingCollection.properties.description"
label="Description"
rows="2"
/>
@@ -240,7 +240,7 @@ watch(() => props.modelValue, async (newValue) => {
<v-row>
<v-col v-if="mode === 'edit'" cols="6">
<v-switch
v-model="editingCollection.enabled"
v-model="editingCollection.properties.visibility"
label="Enabled"
color="primary"
/>

View File

@@ -29,9 +29,9 @@ const filteredCollections = computed(() => {
return collections.value.filter(collection => {
if (props.type === 'calendar') {
return collection.contents?.event
return collection.properties.contents?.event
} else if (props.type === 'tasklist') {
return collection.contents?.task
return collection.properties.contents?.task
}
return true
})
@@ -53,7 +53,7 @@ const displayIcon = computed(() => {
onMounted(async () => {
loading.value = true
try {
collections.value = await collectionsStore.list()
collections.value = Object.values(await collectionsStore.list())
} catch (error) {
console.error('[Chrono] - Failed to load collections:', error)
}
@@ -71,7 +71,7 @@ const onCollectionEdit = (collection: CollectionObject) => {
}
const onToggleVisibility = (collection: CollectionObject) => {
collection.enabled = !collection.enabled
collection.properties.visibility = collection.properties.visibility === false
emit('toggle-visibility', collection)
}
@@ -80,7 +80,7 @@ defineExpose({
async refresh() {
loading.value = true
try {
collections.value = await collectionsStore.list()
collections.value = Object.values(await collectionsStore.list())
} catch (error) {
console.error('[Chrono] - Failed to load collections:', error)
}
@@ -106,28 +106,28 @@ defineExpose({
<v-list v-else density="compact" nav class="pa-0">
<v-list-item
v-for="collection in filteredCollections"
:key="collection.id"
:value="collection.id"
:active="selectedCollection?.id === collection.id"
:key="collection.identifier"
:value="collection.identifier"
:active="selectedCollection?.identifier === collection.identifier"
@click="onCollectionSelect(collection)"
rounded="lg"
class="mb-1"
>
<template #prepend>
<v-checkbox-btn
:model-value="collection.enabled !== false"
:color="collection.color || 'primary'"
:model-value="collection.properties.visibility !== false"
:color="collection.properties.color || 'primary'"
hide-details
@click.stop="onToggleVisibility(collection)"
/>
</template>
<v-list-item-title>{{ collection.label || 'Unnamed Collection' }}</v-list-item-title>
<v-list-item-title>{{ collection.properties.label || 'Unnamed Collection' }}</v-list-item-title>
<template #append>
<div class="d-flex align-center">
<v-icon
:color="collection.color || 'primary'"
:color="collection.properties.color || 'primary'"
size="small"
class="mr-1"
>mdi-circle</v-icon>

View File

@@ -42,7 +42,7 @@
<div class="all-day-events-overlay">
<div
v-for="segment in allDaySegments.segments"
:key="segment.entity.id"
:key="segment.entity.identifier"
class="all-day-event"
:class="{
'is-start': segment.isStart,
@@ -53,7 +53,7 @@
@mouseenter="emit('event-hover', { event: $event, entity: segment.entity })"
@mouseleave="emit('event-hover-end')"
>
<span v-if="segment.isStart" class="event-label">{{ segment.entity.data?.label || 'Untitled' }}</span>
<span v-if="segment.isStart" class="event-label">{{ segment.entity.properties?.label || 'Untitled' }}</span>
</div>
</div>
</div>
@@ -71,7 +71,7 @@
<div class="day-events" @click="handleDayClick($event, day)">
<div
v-for="entity in getTimedEvents(day)"
:key="entity.id"
:key="entity.identifier"
class="day-event"
:style="getEventStyle(entity)"
@click.stop="emit('event-click', entity)"
@@ -80,12 +80,12 @@
>
<template v-if="daysCount <= 3">
<div class="event-time">
{{ entity.data?.startsOn ? formatTime(new Date(entity.data.startsOn)) : '' }} - {{ entity.data?.endsOn ? formatTime(new Date(entity.data.endsOn)) : '' }}
{{ entity.properties?.startsOn ? formatTime(new Date(entity.properties.startsOn)) : '' }} - {{ entity.properties?.endsOn ? formatTime(new Date(entity.properties.endsOn)) : '' }}
</div>
<div class="event-title">{{ entity.data?.label || 'Untitled' }}</div>
<div class="event-title">{{ entity.properties?.label || 'Untitled' }}</div>
</template>
<template v-else>
<div class="event-title-compact">{{ entity.data?.label || 'Untitled' }}</div>
<div class="event-title-compact">{{ entity.properties?.label || 'Untitled' }}</div>
</template>
</div>
</div>
@@ -200,9 +200,9 @@ function getTimedEvents(date: Date): any[] {
}
function getEventColor(entity: any): string {
if (entity.data?.color) return entity.data.color;
const calendar = props.calendars.find(cal => cal.id === entity.in);
return calendar?.color || '#1976D2';
if (entity.properties?.color) return entity.properties.color;
const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
return calendar?.properties?.color || '#1976D2';
}
function getAllDayEventStyle(segment: MultiDaySegment) {
@@ -215,11 +215,11 @@ function getAllDayEventStyle(segment: MultiDaySegment) {
}
function getEventStyle(entity: any) {
if (!entity.data?.startsOn || !entity.data?.endsOn) {
if (!entity.properties?.startsOn || !entity.properties?.endsOn) {
return { display: 'none' };
}
const startTime = new Date(entity.data.startsOn);
const endTime = new Date(entity.data.endsOn);
const startTime = new Date(entity.properties.startsOn);
const endTime = new Date(entity.properties.endsOn);
const start = startTime.getHours() * 60 + startTime.getMinutes();
const duration = (endTime.getTime() - startTime.getTime()) / (1000 * 60);

View File

@@ -32,18 +32,18 @@ const emit = defineEmits<{
// State
const loading = ref(false)
const saving = ref(false)
const selectedCollectionId = ref(props.entity?.in || props.collection?.id || props.calendars?.[0]?.id || '')
const selectedCollectionId = ref(props.entity?.collection || props.collection?.identifier || props.calendars?.[0]?.identifier || '')
// Computed
const mode = computed(() => props.mode)
const entity = computed(() => props.entity || null)
const entityObject = computed(() => entity.value?.data as EventObject ?? null)
const entityFresh = computed(() => entity.value?.id === null || entity.value?.id === undefined)
const entityObject = computed(() => entity.value?.properties as EventObject ?? null)
const entityFresh = computed(() => !entity.value?.identifier)
const calendarOptions = computed(() =>
(props.calendars || []).map(cal => ({
title: cal.label || 'Unnamed Calendar',
value: cal.id,
title: cal.properties.label || 'Unnamed Calendar',
value: cal.identifier,
}))
)
@@ -74,7 +74,7 @@ const cancelEdit = () => {
const saveEntity = async () => {
saving.value = true
const targetCollection = (props.calendars || []).find(cal => cal.id === selectedCollectionId.value)
const targetCollection = (props.calendars || []).find(cal => cal.identifier === selectedCollectionId.value)
emit('save', entity.value!, targetCollection || props.collection)
saving.value = false
}
@@ -161,7 +161,7 @@ const removeNotification = (key: string) => {
<div v-else class="pa-4">
<v-select
v-if="mode === 'edit' && !entity.id"
v-if="mode === 'edit' && !entity.identifier"
v-model="selectedCollectionId"
:items="calendarOptions"
label="Calendar"

View File

@@ -41,25 +41,25 @@ const popupStyle = computed(() => {
})
const hasLocation = computed(() => {
return props.event?.data?.locationsPhysical && Object.keys(props.event.data.locationsPhysical).length > 0 ||
props.event?.data?.locationsVirtual && Object.keys(props.event.data.locationsVirtual).length > 0
return props.event?.properties?.locationsPhysical && Object.keys(props.event.properties.locationsPhysical).length > 0 ||
props.event?.properties?.locationsVirtual && Object.keys(props.event.properties.locationsVirtual).length > 0
})
const firstLocation = computed(() => {
if (props.event?.data?.locationsPhysical && Object.keys(props.event.data.locationsPhysical).length > 0) {
const key = Object.keys(props.event.data.locationsPhysical)[0]
return props.event.data.locationsPhysical[key]
if (props.event?.properties?.locationsPhysical && Object.keys(props.event.properties.locationsPhysical).length > 0) {
const key = Object.keys(props.event.properties.locationsPhysical)[0]
return props.event.properties.locationsPhysical[key]
}
if (props.event?.data?.locationsVirtual && Object.keys(props.event.data.locationsVirtual).length > 0) {
const key = Object.keys(props.event.data.locationsVirtual)[0]
return props.event.data.locationsVirtual[key]
if (props.event?.properties?.locationsVirtual && Object.keys(props.event.properties.locationsVirtual).length > 0) {
const key = Object.keys(props.event.properties.locationsVirtual)[0]
return props.event.properties.locationsVirtual[key]
}
return null
})
const participantCount = computed(() => {
if (!props.event?.data?.participants) return 0
return Object.keys(props.event.data.participants).length
if (!props.event?.properties?.participants) return 0
return Object.keys(props.event.properties.participants).length
})
</script>
@@ -75,18 +75,18 @@ const participantCount = computed(() => {
<v-card-text class="pa-3">
<div class="d-flex align-center mb-2">
<v-icon icon="mdi-calendar" size="small" class="mr-2" />
<div class="text-subtitle-1 font-weight-bold">{{ event.data?.label || 'Untitled Event' }}</div>
<div class="text-subtitle-1 font-weight-bold">{{ event.properties?.label || 'Untitled Event' }}</div>
</div>
<div class="event-popup-details">
<div class="d-flex align-center mb-1">
<v-icon icon="mdi-clock-outline" size="small" class="mr-2 text-grey" />
<div class="text-caption">
<div v-if="event.data?.timeless">
All Day - {{ new Date(event.data.startsOn).toLocaleDateString() }}
<div v-if="event.properties?.timeless">
All Day - {{ new Date(event.properties.startsOn).toLocaleDateString() }}
</div>
<div v-else>
{{ formatTime(event.data?.startsOn) }} - {{ formatTime(event.data?.endsOn) }}
{{ formatTime(event.properties?.startsOn) }} - {{ formatTime(event.properties?.endsOn) }}
</div>
</div>
</div>
@@ -98,10 +98,10 @@ const participantCount = computed(() => {
</div>
</div>
<div v-if="event.data?.organizer" class="d-flex align-center mb-1">
<div v-if="event.properties?.organizer" class="d-flex align-center mb-1">
<v-icon icon="mdi-account" size="small" class="mr-2 text-grey" />
<div class="text-caption">
{{ event.data.organizer.name || event.data.organizer.address }}
{{ event.properties.organizer.name || event.properties.organizer.address }}
</div>
</div>
@@ -112,13 +112,13 @@ const participantCount = computed(() => {
</div>
</div>
<div v-if="event.data?.description" class="mt-2 pt-2 border-t">
<div class="text-caption text-grey">{{ event.data.description }}</div>
<div v-if="event.properties?.description" class="mt-2 pt-2 border-t">
<div class="text-caption text-grey">{{ event.properties.description }}</div>
</div>
<div v-if="event.data?.tags && event.data.tags.length > 0" class="mt-2">
<div v-if="event.properties?.tags && event.properties.tags.length > 0" class="mt-2">
<v-chip
v-for="(tag, index) in event.data.tags.slice(0, 3)"
v-for="(tag, index) in event.properties.tags.slice(0, 3)"
:key="index"
size="x-small"
class="mr-1"
@@ -126,8 +126,8 @@ const participantCount = computed(() => {
>
{{ tag }}
</v-chip>
<span v-if="event.data.tags.length > 3" class="text-caption text-grey">
+{{ event.data.tags.length - 3 }} more
<span v-if="event.properties.tags.length > 3" class="text-caption text-grey">
+{{ event.properties.tags.length - 3 }} more
</span>
</div>
</div>

View File

@@ -141,18 +141,18 @@ function getMultiDaySegments(weekStart: Date, weekEnd: Date): { segments: MultiD
// Filter multiday events that overlap this week
const multiDayEvents = props.events.filter(entity => {
if (!entity.data?.startsOn || !isMultiDay(entity)) return false;
const eventStart = startOfDay(new Date(entity.data.startsOn));
const eventEnd = startOfDay(new Date(entity.data.endsOn));
if (!entity.properties?.startsOn || !isMultiDay(entity)) return false;
const eventStart = startOfDay(new Date(entity.properties.startsOn));
const eventEnd = startOfDay(new Date(entity.properties.endsOn));
return eventStart <= weekEnd && eventEnd >= weekStart;
});
// Sort: longer events first, then by start date
multiDayEvents.sort((a, b) => {
const aStart = new Date(a.data.startsOn);
const aEnd = new Date(a.data.endsOn);
const bStart = new Date(b.data.startsOn);
const bEnd = new Date(b.data.endsOn);
const aStart = new Date(a.properties.startsOn);
const aEnd = new Date(a.properties.endsOn);
const bStart = new Date(b.properties.startsOn);
const bEnd = new Date(b.properties.endsOn);
const aDuration = daysDiff(aStart, aEnd);
const bDuration = daysDiff(bStart, bEnd);
if (bDuration !== aDuration) return bDuration - aDuration;
@@ -163,8 +163,8 @@ function getMultiDaySegments(weekStart: Date, weekEnd: Date): { segments: MultiD
const lanes: boolean[][] = []; // lanes[lane][dayOfWeek] = occupied
for (const entity of multiDayEvents) {
const eventStart = startOfDay(new Date(entity.data.startsOn));
const eventEnd = startOfDay(new Date(entity.data.endsOn));
const eventStart = startOfDay(new Date(entity.properties.startsOn));
const eventEnd = startOfDay(new Date(entity.properties.endsOn));
// Clamp to this week
const segStart = eventStart < weekStart ? weekStart : eventStart;
@@ -226,9 +226,9 @@ function isToday(date: Date): boolean {
}
function getEventColor(entity: any): string {
if (entity.data?.color) return entity.data.color;
const calendar = props.calendars.find(cal => cal.id === entity.in);
return calendar?.color || '#1976D2';
if (entity.properties?.color) return entity.properties.color;
const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
return calendar?.properties?.color || '#1976D2';
}
</script>
@@ -276,14 +276,14 @@ function getEventColor(entity: any): string {
<div class="cell-events">
<div
v-for="entity in getSingleDayEvents(cell.date).slice(0, MAX_VISIBLE_EVENTS - week.laneCount)"
:key="entity.id"
:key="entity.identifier"
class="single-day-event"
:style="{ backgroundColor: getEventColor(entity) }"
@click.stop="$emit('event-click', entity)"
@mouseenter="$emit('event-hover', { event: $event, entity })"
@mouseleave="$emit('event-hover-end')"
>
{{ entity.data?.label || 'Untitled' }}
{{ entity.properties?.label || 'Untitled' }}
</div>
<div
v-if="getHiddenCount(cell, week.laneCount) > 0"
@@ -300,7 +300,7 @@ function getEventColor(entity: any): string {
<div class="multiday-overlay">
<div
v-for="segment in week.multiDaySegments"
:key="`${segment.entity.id}-${weekIndex}`"
:key="`${segment.entity.identifier}-${weekIndex}`"
class="multiday-event"
:class="{
'is-start': segment.isStart,
@@ -317,7 +317,7 @@ function getEventColor(entity: any): string {
@mouseleave="$emit('event-hover-end')"
>
<span v-if="segment.isStart" class="event-label">
{{ segment.entity.data?.label || 'Untitled' }}
{{ segment.entity.properties?.label || 'Untitled' }}
</span>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<v-card v-if="entity" class="task-editor">
<v-card-title class="d-flex justify-space-between align-center">
<span>{{ entity.id ? entity.data?.label || 'Untitled Task' : 'New Task' }}</span>
<span>{{ entity.identifier ? entity.properties?.label || 'Untitled Task' : 'New Task' }}</span>
<div class="d-flex gap-2">
<v-btn
v-if="mode === 'view' && canEdit"
@@ -22,7 +22,7 @@
<v-card-text class="task-editor-content">
<v-form ref="formRef">
<v-text-field
v-model="entity.data.label"
v-model="entity.properties.label"
label="Title"
:rules="[v => !!v || 'Title is required']"
:readonly="!isEditing"
@@ -40,7 +40,7 @@
></v-select>
<v-textarea
v-model="entity.data.description"
v-model="entity.properties.description"
label="Description"
:readonly="!isEditing"
:variant="isEditing ? 'outlined' : 'plain'"
@@ -49,7 +49,7 @@
></v-textarea>
<v-select
v-model="entity.data.priority"
v-model="entity.properties.priority"
:items="priorityOptions"
label="Priority"
:readonly="!isEditing"
@@ -78,7 +78,7 @@
></v-text-field>
<v-combobox
v-model="entity.data.categories"
v-model="entity.properties.categories"
label="Tags"
multiple
chips
@@ -90,7 +90,7 @@
<div class="d-flex gap-4">
<v-text-field
v-model.number="entity.data.estimatedTime"
v-model.number="entity.properties.estimatedTime"
label="Estimated Time (minutes)"
type="number"
:readonly="!isEditing"
@@ -98,7 +98,7 @@
class="flex-1-1"
></v-text-field>
<v-text-field
v-model.number="entity.data.actualTime"
v-model.number="entity.properties.actualTime"
label="Actual Time (minutes)"
type="number"
:readonly="!isEditing"
@@ -108,13 +108,13 @@
</div>
<!-- Metadata for view mode -->
<div v-if="mode === 'view' && entity.data.created" class="mt-4">
<div v-if="mode === 'view' && entity.properties.created" class="mt-4">
<v-divider class="mb-4"></v-divider>
<div class="text-caption text-medium-emphasis">
<div>Created: {{ new Date(entity.data.created).toLocaleString() }}</div>
<div v-if="entity.data.modified">Modified: {{ new Date(entity.data.modified).toLocaleString() }}</div>
<div v-if="entity.data.status === 'completed' && entity.data.completedOn">
Completed: {{ new Date(entity.data.completedOn).toLocaleString() }}
<div>Created: {{ new Date(entity.properties.created).toLocaleString() }}</div>
<div v-if="entity.properties.modified">Modified: {{ new Date(entity.properties.modified).toLocaleString() }}</div>
<div v-if="entity.properties.status === 'completed' && entity.properties.completedOn">
Completed: {{ new Date(entity.properties.completedOn).toLocaleString() }}
</div>
<div v-if="isExternalTask" class="mt-2">
<v-chip size="small" color="info" variant="tonal">
@@ -129,7 +129,7 @@
<v-divider></v-divider>
<v-card-actions>
<v-btn
v-if="mode === 'edit' && entity.id && canDelete"
v-if="mode === 'edit' && entity.identifier && canDelete"
color="error"
variant="text"
@click="handleDelete"
@@ -182,17 +182,17 @@ const emit = defineEmits<{
}>();
const formRef = ref();
const selectedCollectionId = ref(props.entity?.in || props.collection?.id || props.lists[0]?.id || '');
const selectedCollectionId = ref(props.entity?.collection || props.collection?.identifier || props.lists[0]?.identifier || '');
// Determine permissions based on entity metadata
const isExternalTask = computed(() => {
// Check if task was assigned by another user
return props.entity?.data?.external === true || props.entity?.data?.assignedBy;
return props.entity?.properties?.external === true || props.entity?.properties?.assignedBy;
});
const canEdit = computed(() => {
if (!props.entity) return false;
if (props.entity.data?.readonly === true) return false;
if (props.entity.properties?.readonly === true) return false;
return true;
});
@@ -203,12 +203,12 @@ const canEditDates = computed(() => {
const canChangeCollection = computed(() => {
// Can't move external tasks to different lists
return !isExternalTask.value && !props.entity?.id;
return !isExternalTask.value && !props.entity?.identifier;
});
const canDelete = computed(() => {
// Can't delete external/readonly tasks
return !isExternalTask.value && props.entity?.data?.readonly !== true;
return !isExternalTask.value && props.entity?.properties?.readonly !== true;
});
const isEditing = computed(() => props.mode === 'edit');
@@ -218,25 +218,25 @@ const startDate = ref('');
// Watch for entity changes to update date fields
watch(() => props.entity, (newEntity) => {
if (newEntity?.data?.dueOn) {
dueDate.value = formatDate(new Date(newEntity.data.dueOn));
if (newEntity?.properties?.dueOn) {
dueDate.value = formatDate(new Date(newEntity.properties.dueOn));
} else {
dueDate.value = '';
}
if (newEntity?.data?.startsOn) {
startDate.value = formatDate(new Date(newEntity.data.startsOn));
if (newEntity?.properties?.startsOn) {
startDate.value = formatDate(new Date(newEntity.properties.startsOn));
} else {
startDate.value = '';
}
selectedCollectionId.value = newEntity?.in || props.collection?.id || props.lists[0]?.id || '';
selectedCollectionId.value = newEntity?.collection || props.collection?.identifier || props.lists[0]?.identifier || '';
}, { immediate: true });
const listOptions = computed(() =>
props.lists.map(list => ({
title: list.label || 'Unnamed List',
value: list.id,
title: list.properties.label || 'Unnamed List',
value: list.identifier,
}))
);
@@ -248,13 +248,13 @@ const priorityOptions = [
watch(dueDate, (newVal) => {
if (props.entity && isEditing.value) {
props.entity.data.dueOn = newVal ? new Date(newVal).toISOString() : null;
props.entity.properties.dueOn = newVal ? new Date(newVal).toISOString() : null;
}
});
watch(startDate, (newVal) => {
if (props.entity && isEditing.value) {
props.entity.data.startsOn = newVal ? new Date(newVal).toISOString() : null;
props.entity.properties.startsOn = newVal ? new Date(newVal).toISOString() : null;
}
});
@@ -266,7 +266,7 @@ async function handleSave() {
const { valid } = await formRef.value.validate();
if (!valid) return;
const targetCollection = props.lists.find(list => list.id === selectedCollectionId.value);
const targetCollection = props.lists.find(list => list.identifier === selectedCollectionId.value);
emit('save', props.entity, targetCollection || props.collection);
}
@@ -276,7 +276,7 @@ function handleCancel() {
function handleDelete() {
if (props.entity) {
const targetCollection = props.lists.find(list => list.id === selectedCollectionId.value);
const targetCollection = props.lists.find(list => list.identifier === selectedCollectionId.value);
emit('delete', props.entity, targetCollection || props.collection);
}
}

View File

@@ -21,41 +21,41 @@
<v-list>
<v-list-item
v-for="entity in filteredTasks"
:key="entity.id"
:key="entity.identifier"
class="task-item"
:class="{ 'completed': entity.data?.status === 'completed' }"
:class="{ 'completed': entity.properties?.status === 'completed' }"
@click="$emit('task-click', entity)"
>
<template #prepend>
<v-checkbox-btn
:model-value="entity.data?.status === 'completed'"
:model-value="entity.properties?.status === 'completed'"
hide-details
@click.stop="$emit('toggle-complete', entity.id)"
@click.stop="$emit('toggle-complete', entity.identifier)"
></v-checkbox-btn>
</template>
<div class="task-content">
<div class="task-title" :class="{ 'completed': entity.data?.status === 'completed' }">
{{ entity.data?.label || 'Untitled Task' }}
<div class="task-title" :class="{ 'completed': entity.properties?.status === 'completed' }">
{{ entity.properties?.label || 'Untitled Task' }}
</div>
<div v-if="entity.data?.description" class="task-description text-caption">
{{ entity.data.description }}
<div v-if="entity.properties?.description" class="task-description text-caption">
{{ entity.properties.description }}
</div>
<div class="task-meta">
<span v-if="entity.data?.dueOn" class="due-date">
<span v-if="entity.properties?.dueOn" class="due-date">
<v-icon size="small">mdi-calendar</v-icon>
{{ formatDueDate(new Date(entity.data.dueOn)) }}
{{ formatDueDate(new Date(entity.properties.dueOn)) }}
</span>
<v-chip
v-if="entity.data?.priority"
v-if="entity.properties?.priority"
size="x-small"
:class="`priority-${entity.data.priority === 1 ? 'high' : entity.data.priority === 2 ? 'medium' : 'low'}`"
:class="`priority-${entity.properties.priority === 1 ? 'high' : entity.properties.priority === 2 ? 'medium' : 'low'}`"
class="priority-badge"
>
{{ entity.data.priority === 1 ? 'high' : entity.data.priority === 2 ? 'medium' : 'low' }}
{{ entity.properties.priority === 1 ? 'high' : entity.properties.priority === 2 ? 'medium' : 'low' }}
</v-chip>
<v-chip
v-for="tag in (entity.data?.categories || [])"
v-for="tag in (entity.properties?.categories || [])"
:key="tag"
size="x-small"
variant="outlined"
@@ -100,34 +100,34 @@ const filteredTasks = computed(() => {
// Filter by tab
if (activeTab.value === 'active') {
filtered = filtered.filter(entity => entity.data?.status !== 'completed');
filtered = filtered.filter(entity => entity.properties?.status !== 'completed');
} else if (activeTab.value === 'completed') {
filtered = filtered.filter(entity => entity.data?.status === 'completed');
filtered = filtered.filter(entity => entity.properties?.status === 'completed');
}
// Filter by priority
if (filterPriority.value) {
const priorityMap = { high: 1, medium: 2, low: 3 };
const targetPriority = priorityMap[filterPriority.value];
filtered = filtered.filter(entity => entity.data?.priority === targetPriority);
filtered = filtered.filter(entity => entity.properties?.priority === targetPriority);
}
// Sort: incomplete first, then by due date, then by priority
return filtered.sort((a, b) => {
const aCompleted = a.data?.status === 'completed';
const bCompleted = b.data?.status === 'completed';
const aCompleted = a.properties?.status === 'completed';
const bCompleted = b.properties?.status === 'completed';
if (aCompleted !== bCompleted) {
return aCompleted ? 1 : -1;
}
if (a.data?.dueOn && b.data?.dueOn) {
return new Date(a.data.dueOn).getTime() - new Date(b.data.dueOn).getTime();
if (a.properties?.dueOn && b.properties?.dueOn) {
return new Date(a.properties.dueOn).getTime() - new Date(b.properties.dueOn).getTime();
}
if (a.data?.dueOn) return -1;
if (b.data?.dueOn) return 1;
if (a.properties?.dueOn) return -1;
if (b.properties?.dueOn) return 1;
return (a.data?.priority || 4) - (b.data?.priority || 4);
return (a.properties?.priority || 4) - (b.properties?.priority || 4);
});
});

View File

@@ -8,6 +8,8 @@ import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore';
import { useServicesStore } from '@ChronoManager/stores/servicesStore';
import { CollectionObject } from '@ChronoManager/models/collection';
import { EntityObject } from '@ChronoManager/models/entity';
import { EventObject } from '@ChronoManager/models/event';
import { TaskObject } from '@ChronoManager/models/task';
import { ServiceObject } from '@ChronoManager/models/service';
import type { CalendarView as CalendarViewType } from '@/types';
import CollectionList from '@/components/CollectionList.vue';
@@ -45,19 +47,19 @@ const selectedCollection = ref<CollectionObject | null>(null);
// Computed - filter collections and entities
const calendars = computed(() => {
return collections.value.filter(col => col.contents?.event);
return collections.value.filter(col => col.properties.contents?.event);
});
const taskLists = computed(() => {
return collections.value.filter(col => col.contents?.task);
return collections.value.filter(col => col.properties.contents?.task);
});
const events = computed(() => {
return entities.value.filter(entity => entity.data && (entity.data as any).type === 'event');
return entities.value.filter(entity => entity.properties && (entity.properties as any).type === 'event');
});
const tasks = computed(() => {
return entities.value.filter(entity => entity.data && (entity.data as any).type === 'task');
return entities.value.filter(entity => entity.properties && (entity.properties as any).type === 'task');
});
// Dialog state
@@ -75,11 +77,11 @@ const isTaskView = computed(() => viewMode.value === 'tasks');
const filteredEvents = computed(() => {
const visibleCalendarIds = calendars.value
.filter(cal => cal.enabled !== false)
.map(cal => cal.id);
.filter(cal => cal.properties.visibility !== false)
.map(cal => cal.identifier);
return events.value.filter(event =>
visibleCalendarIds.includes(event.in)
visibleCalendarIds.includes(event.collection)
);
});
@@ -94,8 +96,8 @@ function selectCalendar(calendar: CollectionObject) {
}
function createCalendar() {
editingCollection.value = collectionsStore.fresh();
editingCollection.value.contents = { event: true };
editingCollection.value = new CollectionObject();
editingCollection.value.properties.contents = { event: true };
collectionEditorMode.value = 'create';
collectionEditorType.value = 'calendar';
showCollectionEditor.value = true;
@@ -110,7 +112,12 @@ function editCalendar(collection: CollectionObject) {
async function toggleCalendarVisibility(collection: CollectionObject) {
try {
await collectionsStore.modify(collection);
await collectionsStore.update(
collection.provider,
collection.service,
collection.identifier,
collection.properties
);
console.log('[Chrono] - Toggled calendar visibility:', collection);
} catch (error) {
console.error('[Chrono] - Failed to toggle calendar visibility:', error);
@@ -123,8 +130,8 @@ function selectTaskList(list: CollectionObject) {
}
function createTaskList() {
editingCollection.value = collectionsStore.fresh();
editingCollection.value.contents = { task: true };
editingCollection.value = new CollectionObject();
editingCollection.value.properties.contents = { task: true };
collectionEditorMode.value = 'create';
collectionEditorType.value = 'tasklist';
showCollectionEditor.value = true;
@@ -148,9 +155,8 @@ function createEvent() {
return;
}
// Create fresh event entity
selectedEntity.value = entitiesStore.fresh('event');
selectedEntity.value.in = selectedCollection.value.id;
selectedEntity.value = new EntityObject();
selectedEntity.value.properties = new EventObject();
entityEditorMode.value = 'edit';
showEventEditor.value = true;
}
@@ -171,15 +177,25 @@ async function saveEvent(entity: EntityObject, collection?: CollectionObject | n
return;
}
if (entity.data) {
entity.data.modified = new Date();
}
const eventData = entity.properties as EventObject;
eventData.modified = new Date().toISOString();
if (entity.id === null) {
entity.data.created = new Date();
selectedEntity.value = await entitiesStore.create(collection, entity);
if (!entity.identifier) {
eventData.created = new Date().toISOString();
selectedEntity.value = await entitiesStore.create(
collection.provider,
collection.service,
collection.identifier,
eventData.toJson()
);
} else {
selectedEntity.value = await entitiesStore.modify(collection, entity);
selectedEntity.value = await entitiesStore.update(
collection.provider,
collection.service,
collection.identifier,
entity.identifier,
eventData.toJson()
);
}
entityEditorMode.value = 'view';
@@ -191,14 +207,14 @@ async function saveEvent(entity: EntityObject, collection?: CollectionObject | n
async function deleteEvent(entity: EntityObject, collection?: CollectionObject | null) {
try {
if (!(collection instanceof CollectionObject)) {
collection = collections.value.find(c => c.id === entity.in);
collection = collections.value.find(c => c.identifier === entity.collection);
}
if (!collection) {
console.error('[Chrono] - No collection found');
return;
}
await entitiesStore.destroy(collection, entity);
await entitiesStore.delete(collection.provider, collection.service, collection.identifier, entity.identifier);
selectedEntity.value = null;
entityEditorMode.value = 'view';
showEventEditor.value = false;
@@ -218,12 +234,11 @@ function handleDateClick(date: Date) {
return;
}
selectedEntity.value = entitiesStore.fresh('event');
selectedEntity.value.in = selectedCollection.value.id;
if (selectedEntity.value.data) {
selectedEntity.value.data.startsOn = date;
selectedEntity.value.data.endsOn = new Date(date.getTime() + 60 * 60 * 1000);
}
selectedEntity.value = new EntityObject();
selectedEntity.value.properties = new EventObject();
const eventData = selectedEntity.value.properties as EventObject;
eventData.startsOn = date.toISOString();
eventData.endsOn = new Date(date.getTime() + 60 * 60 * 1000).toISOString();
entityEditorMode.value = 'edit';
showEventEditor.value = true;
}
@@ -257,9 +272,8 @@ function createTask() {
return;
}
// Create fresh task entity
selectedEntity.value = entitiesStore.fresh('task');
selectedEntity.value.in = selectedCollection.value.id;
selectedEntity.value = new EntityObject();
selectedEntity.value.properties = new TaskObject();
entityEditorMode.value = 'edit';
showTaskEditor.value = true;
}
@@ -280,15 +294,25 @@ async function saveTask(entity: EntityObject, collection?: CollectionObject | nu
return;
}
if (entity.data) {
entity.data.modified = new Date();
}
const taskData = entity.properties as TaskObject;
taskData.modified = new Date().toISOString();
if (entity.id === null) {
entity.data.created = new Date();
selectedEntity.value = await entitiesStore.create(collection, entity);
if (!entity.identifier) {
taskData.created = new Date().toISOString();
selectedEntity.value = await entitiesStore.create(
collection.provider,
collection.service,
collection.identifier,
taskData.toJson()
);
} else {
selectedEntity.value = await entitiesStore.modify(collection, entity);
selectedEntity.value = await entitiesStore.update(
collection.provider,
collection.service,
collection.identifier,
entity.identifier,
taskData.toJson()
);
}
entityEditorMode.value = 'view';
@@ -300,14 +324,14 @@ async function saveTask(entity: EntityObject, collection?: CollectionObject | nu
async function deleteTask(entity: EntityObject, collection?: CollectionObject | null) {
try {
if (!(collection instanceof CollectionObject)) {
collection = collections.value.find(c => c.id === entity.in);
collection = collections.value.find(c => c.identifier === entity.collection);
}
if (!collection) {
console.error('[Chrono] - No collection found');
return;
}
await entitiesStore.destroy(collection, entity);
await entitiesStore.delete(collection.provider, collection.service, collection.identifier, entity.identifier);
selectedEntity.value = null;
entityEditorMode.value = 'view';
showTaskEditor.value = false;
@@ -318,21 +342,27 @@ async function deleteTask(entity: EntityObject, collection?: CollectionObject |
async function toggleTaskComplete(taskId: string | number) {
try {
const entity = entities.value.find(e => e.id === taskId);
if (!entity || !entity.data) return;
const entity = entities.value.find(e => e.identifier === taskId);
if (!entity || !entity.properties) return;
const collection = collections.value.find(c => c.id === entity.in);
const collection = collections.value.find(c => c.identifier === entity.collection);
if (!collection) return;
const taskData = entity.data as any;
const taskData = entity.properties as TaskObject;
const isCompleted = taskData.status === 'completed';
taskData.status = isCompleted ? 'needs-action' : 'completed';
taskData.completedOn = isCompleted ? null : new Date();
taskData.completedOn = isCompleted ? null : new Date().toISOString();
taskData.progress = isCompleted ? null : 100;
taskData.modified = new Date();
taskData.modified = new Date().toISOString();
await entitiesStore.modify(collection, entity);
await entitiesStore.update(
collection.provider,
collection.service,
collection.identifier,
entity.identifier,
taskData.toJson()
);
} catch (error) {
console.error('[Chrono] - Failed to toggle task completion:', error);
}
@@ -341,14 +371,24 @@ async function toggleTaskComplete(taskId: string | number) {
async function saveCollection(collection: CollectionObject, service: ServiceObject) {
try {
if (collectionEditorMode.value === 'create') {
await collectionsStore.create(service, collection);
await collectionsStore.create(
service.provider,
service.identifier || '',
null,
collection.properties
);
console.log('[Chrono] - Created collection:', collection);
} else {
await collectionsStore.modify(collection);
await collectionsStore.update(
collection.provider,
collection.service,
collection.identifier,
collection.properties
);
console.log('[Chrono] - Modified collection:', collection);
}
// Reload collections
collections.value = await collectionsStore.list();
collections.value = Object.values(await collectionsStore.list());
} catch (error) {
console.error('[Chrono] - Failed to save collection:', error);
}
@@ -356,11 +396,11 @@ async function saveCollection(collection: CollectionObject, service: ServiceObje
async function deleteCollection(collection: CollectionObject) {
try {
await collectionsStore.destroy(collection);
await collectionsStore.delete(collection.provider, collection.service, collection.identifier);
console.log('[Chrono] - Deleted collection:', collection);
// Reload collections
collections.value = await collectionsStore.list();
if (selectedCollection.value?.id === collection.id) {
collections.value = Object.values(await collectionsStore.list());
if (selectedCollection.value?.identifier === collection.identifier) {
selectedCollection.value = null;
}
} catch (error) {
@@ -372,10 +412,28 @@ async function deleteCollection(collection: CollectionObject) {
onMounted(async () => {
try {
// Load collections (calendars and task lists)
collections.value = await collectionsStore.list();
collections.value = Object.values(await collectionsStore.list());
// Load entities (events and tasks)
entities.value = await entitiesStore.list(null, null, null);
// Load entities (events and tasks) for available collection sources only
const sources = collections.value.reduce<Record<string, Record<string, Record<string | number, true>>>>((acc, collection) => {
if (!acc[collection.provider]) {
acc[collection.provider] = {};
}
const serviceKey = String(collection.service);
if (!acc[collection.provider][serviceKey]) {
acc[collection.provider][serviceKey] = {};
}
acc[collection.provider][serviceKey][collection.identifier] = true;
return acc;
}, {});
if (Object.keys(sources).length > 0) {
entities.value = Object.values(await entitiesStore.list(sources));
} else {
entities.value = [];
}
console.log('[Chrono] - Loaded data from ChronoManager:', {
collections: collections.value.length,

View File

@@ -38,9 +38,9 @@ export function daysDiff(a: Date, b: Date): number {
* Check if an event spans multiple days
*/
export function isMultiDay(entity: any): boolean {
if (!entity.data?.startsOn) return false;
const start = startOfDay(new Date(entity.data.startsOn));
const end = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : start;
if (!entity.properties?.startsOn) return false;
const start = startOfDay(new Date(entity.properties.startsOn));
const end = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : start;
return daysDiff(start, end) > 0;
}
@@ -48,10 +48,10 @@ export function isMultiDay(entity: any): boolean {
* Check if an event is an all-day event (no specific time, or spans full day)
*/
export function isAllDay(entity: any): boolean {
if (!entity.data?.startsOn) return false;
if (!entity.properties?.startsOn) return false;
// Explicit all-day flag
if (entity.data?.allDay === true) return true;
if (entity.properties?.timeless === true) return true;
// Multi-day events are treated as all-day in the days view
if (isMultiDay(entity)) return true;
@@ -63,9 +63,9 @@ export function isAllDay(entity: any): boolean {
* Check if an event overlaps with a date range
*/
export function eventOverlapsRange(entity: any, rangeStart: Date, rangeEnd: Date): boolean {
if (!entity.data?.startsOn) return false;
const eventStart = startOfDay(new Date(entity.data.startsOn));
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : eventStart;
if (!entity.properties?.startsOn) return false;
const eventStart = startOfDay(new Date(entity.properties.startsOn));
const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
return eventStart <= rangeEnd && eventEnd >= rangeStart;
}
@@ -73,9 +73,9 @@ export function eventOverlapsRange(entity: any, rangeStart: Date, rangeEnd: Date
* Check if an event occurs on a specific date
*/
export function eventOnDate(entity: any, date: Date): boolean {
if (!entity.data?.startsOn) return false;
const eventStart = startOfDay(new Date(entity.data.startsOn));
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : eventStart;
if (!entity.properties?.startsOn) return false;
const eventStart = startOfDay(new Date(entity.properties.startsOn));
const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
const targetDate = startOfDay(date);
return eventStart <= targetDate && eventEnd >= targetDate;
}
@@ -95,17 +95,17 @@ export function getMultiDaySegments(
// Filter multi-day/all-day events that overlap this range
const multiDayEvents = events.filter(entity => {
if (!entity.data?.startsOn) return false;
if (!entity.properties?.startsOn) return false;
if (!isAllDay(entity)) return false;
return eventOverlapsRange(entity, rangeStart, rangeEnd);
});
// Sort: longer events first, then by start date
multiDayEvents.sort((a, b) => {
const aStart = new Date(a.data.startsOn);
const aEnd = new Date(a.data.endsOn || a.data.startsOn);
const bStart = new Date(b.data.startsOn);
const bEnd = new Date(b.data.endsOn || b.data.startsOn);
const aStart = new Date(a.properties.startsOn);
const aEnd = new Date(a.properties.endsOn || a.properties.startsOn);
const bStart = new Date(b.properties.startsOn);
const bEnd = new Date(b.properties.endsOn || b.properties.startsOn);
const aDuration = daysDiff(aStart, aEnd);
const bDuration = daysDiff(bStart, bEnd);
if (bDuration !== aDuration) return bDuration - aDuration;
@@ -116,8 +116,8 @@ export function getMultiDaySegments(
const lanes: boolean[][] = [];
for (const entity of multiDayEvents) {
const eventStart = startOfDay(new Date(entity.data.startsOn));
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.endsOn)) : eventStart;
const eventStart = startOfDay(new Date(entity.properties.startsOn));
const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
// Clamp to visible range
const segStart = eventStart < rangeStart ? rangeStart : eventStart;
@@ -173,9 +173,9 @@ export function getMultiDaySegments(
*/
export function getTimedEventsForDate(events: any[], date: Date): any[] {
return events.filter(entity => {
if (!entity.data?.startsOn) return false;
if (!entity.properties?.startsOn) return false;
if (isAllDay(entity)) return false;
const eventDate = startOfDay(new Date(entity.data.startsOn));
const eventDate = startOfDay(new Date(entity.properties.startsOn));
return eventDate.toDateString() === date.toDateString();
});
}
@@ -185,9 +185,9 @@ export function getTimedEventsForDate(events: any[], date: Date): any[] {
*/
export function getSingleDayEventsForDate(events: any[], date: Date): any[] {
return events.filter(entity => {
if (!entity.data?.startsOn) return false;
if (!entity.properties?.startsOn) return false;
if (isMultiDay(entity)) return false;
const eventDate = startOfDay(new Date(entity.data.startsOn));
const eventDate = startOfDay(new Date(entity.properties.startsOn));
return eventDate.toDateString() === date.toDateString();
});
}