refactor: use unified manager design

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-28 14:53:11 -04:00
parent aa96aa4cb4
commit 6657b8a0db
4 changed files with 94 additions and 164 deletions
+5 -5
View File
@@ -2,7 +2,7 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { useServicesStore } from '@ChronoManager/stores/servicesStore' import { useServicesStore } from '@ChronoManager/stores/servicesStore'
import { CollectionObject } from '@ChronoManager/models/collection' import { CollectionObject } from '@ChronoManager/models/collection'
import { ServiceObject } from '@ChronoManager/models/service' import type { ServiceObject } from '@ChronoManager/models/service'
// Store // Store
const servicesStore = useServicesStore() const servicesStore = useServicesStore()
@@ -74,11 +74,11 @@ const onOpen = async () => {
if (props.mode === 'edit') { if (props.mode === 'edit') {
// Edit mode - find the service // Edit mode - find the service
editingCollectionService.value = services.value.find(s => editingCollectionService.value = services.value.find(s =>
s.provider === props.collection!.provider && s.identifier === props.collection!.service `${s.provider}:${s.identifier}` === String(props.collection!.service)
) || null ) || null
} else { } else {
// Create mode - use first service that can create // Create mode - use first service that can create
editingCollectionService.value = services.value.filter(s => s.capabilities?.CollectionCreate)[0] || null editingCollectionService.value = services.value.find(s => s.capable('CollectionCreate')) || null
} }
} }
@@ -142,7 +142,7 @@ watch(() => props.modelValue, async (newValue) => {
<v-combobox v-show="mode === 'create' && services.length > 1" <v-combobox v-show="mode === 'create' && services.length > 1"
v-model="editingCollectionService" v-model="editingCollectionService"
label="Service" label="Service"
:items="services.filter(s => s.capabilities?.CollectionCreate)" :items="services.filter(s => s.capable('CollectionCreate'))"
item-title="label" item-title="label"
item-value="identifier" item-value="identifier"
required required
@@ -254,7 +254,7 @@ watch(() => props.modelValue, async (newValue) => {
<v-card-actions class="justify-space-between align-center"> <v-card-actions class="justify-space-between align-center">
<div> <div>
<v-btn <v-btn
v-if="mode === 'edit' && editingCollectionService?.capabilities?.CollectionDestroy" v-if="mode === 'edit' && editingCollectionService?.capable('CollectionDelete')"
color="error" color="error"
variant="text" variant="text"
@click="onDelete" @click="onDelete"
-96
View File
@@ -1,96 +0,0 @@
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 { useCollectionsStore } from '@ChronoManager/stores/collectionsStore'
import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore'
type ChronoEntityProperties = EventObject | TaskObject
export function useChronoEntityActions(
entitiesStore: ReturnType<typeof useEntitiesStore>,
collectionsStore: ReturnType<typeof useCollectionsStore>,
) {
function getEntityData(entity: EntityObject): Record<string, unknown> {
const properties = entity.properties as ChronoEntityProperties
const now = new Date().toISOString()
properties.modified = now as never
if (!entity.identifier) {
properties.created = now as never
}
return properties.toJson() as unknown as Record<string, unknown>
}
function resolveCollection(entity: EntityObject, collection?: CollectionObject | null): CollectionObject {
if (collection instanceof CollectionObject) {
return collection
}
const foundCollection = collectionsStore.collections.find((item) => item.identifier === entity.collection)
if (!foundCollection) {
throw new Error('Collection not found for entity operation')
}
return foundCollection
}
async function saveEntity(entity: EntityObject, collection: CollectionObject): Promise<EntityObject> {
const data = getEntityData(entity)
if (!entity.identifier) {
return entitiesStore.create(
collection.provider,
collection.service,
collection.identifier,
data,
)
}
return entitiesStore.update(
collection.provider,
collection.service,
collection.identifier,
entity.identifier,
data,
)
}
async function deleteEntity(entity: EntityObject, collection?: CollectionObject | null): Promise<void> {
const resolvedCollection = resolveCollection(entity, collection)
await entitiesStore.delete(
resolvedCollection.provider,
resolvedCollection.service,
resolvedCollection.identifier,
entity.identifier,
)
}
async function toggleTaskCompletion(entity: EntityObject, collection?: CollectionObject | null): Promise<EntityObject> {
const resolvedCollection = resolveCollection(entity, collection)
const taskData = entity.properties as TaskObject
const isCompleted = taskData.status === 'completed'
taskData.status = isCompleted ? 'needs-action' : 'completed'
taskData.completedOn = isCompleted ? null : new Date().toISOString() as never
taskData.progress = isCompleted ? null : 100
taskData.modified = new Date().toISOString() as never
return entitiesStore.update(
resolvedCollection.provider,
resolvedCollection.service,
resolvedCollection.identifier,
entity.identifier,
taskData.toJson(),
)
}
return {
saveEntity,
deleteEntity,
toggleTaskCompletion,
resolveCollection,
}
}
+32 -20
View File
@@ -41,6 +41,7 @@ const {
editingCollection, editingCollection,
collectionEditorMode, collectionEditorMode,
collectionEditorType, collectionEditorType,
loading,
calendars, calendars,
taskLists, taskLists,
isTaskView, isTaskView,
@@ -223,27 +224,31 @@ watch(() => route.fullPath, () => {
</div> </div>
</v-alert> </v-alert>
<CalendarView <div v-if="isChronoManagerAvailable" class="chrono-wrapper">
v-if="!isTaskView" <v-progress-linear v-if="loading" indeterminate color="primary" />
:view="calendarView"
:current-date="currentDate"
:events="filteredEvents"
:calendars="calendars"
:initial-days-span="daysViewSpan"
:initial-agenda-view-span="agendaViewSpan"
@event-click="editEvent"
@date-click="createEventFromDate"
@update:days-span="setDaysViewSpan"
@update:agenda-span="setAgendaViewSpan"
/>
<TaskView <CalendarView
v-else v-if="!isTaskView"
:tasks="filteredTasks" :view="calendarView"
:lists="taskLists" :current-date="currentDate"
@task-click="editTask" :events="filteredEvents"
@toggle-complete="toggleTaskComplete" :calendars="calendars"
/> :initial-days-span="daysViewSpan"
:initial-agenda-view-span="agendaViewSpan"
@event-click="editEvent"
@date-click="createEventFromDate"
@update:days-span="setDaysViewSpan"
@update:agenda-span="setAgendaViewSpan"
/>
<TaskView
v-else
:tasks="filteredTasks"
:lists="taskLists"
@task-click="editTask"
@toggle-complete="toggleTaskComplete"
/>
</div>
</div> </div>
</div> </div>
@@ -322,6 +327,13 @@ watch(() => route.fullPath, () => {
flex-direction: column; flex-direction: column;
} }
.chrono-wrapper {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
}
.border-b { .border-b {
border-bottom: 1px solid rgb(var(--v-border-color)); border-bottom: 1px solid rgb(var(--v-border-color));
} }
+57 -43
View File
@@ -5,10 +5,10 @@ import { CollectionObject } from '@ChronoManager/models/collection'
import { EntityObject } from '@ChronoManager/models/entity' import { EntityObject } from '@ChronoManager/models/entity'
import { EventObject } from '@ChronoManager/models/event' import { EventObject } from '@ChronoManager/models/event'
import { TaskObject } from '@ChronoManager/models/task' import { TaskObject } from '@ChronoManager/models/task'
import { ServiceObject } from '@ChronoManager/models/service' import type { ServiceObject } from '@ChronoManager/models/service'
import { useCollectionsStore } from '@ChronoManager/stores/collectionsStore' import { useCollectionsStore } from '@ChronoManager/stores/collectionsStore'
import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore' import { useEntitiesStore } from '@ChronoManager/stores/entitiesStore'
import type { SourceSelector } from '@ChronoManager/types' import { useServicesStore } from '@ChronoManager/stores/servicesStore'
import type { CalendarView as CalendarViewType } from '@/types' import type { CalendarView as CalendarViewType } from '@/types'
import { import {
AGENDA_VIEW_SPANS, AGENDA_VIEW_SPANS,
@@ -16,15 +16,14 @@ import {
type AgendaViewSpan, type AgendaViewSpan,
type DaysViewSpan, type DaysViewSpan,
} from '@/types/spans' } from '@/types/spans'
import { useChronoEntityActions } from '@/composables/useChronoEntityActions'
type EntitySources = Record<string, Record<string, Record<string | number, true>>> type ChronoEntityProperties = EventObject | TaskObject
export const useChronoStore = defineStore('chronoStore', () => { export const useChronoStore = defineStore('chronoStore', () => {
const userStore = useUserStore() const userStore = useUserStore()
const servicesStore = useServicesStore()
const collectionsStore = useCollectionsStore() const collectionsStore = useCollectionsStore()
const entitiesStore = useEntitiesStore() const entitiesStore = useEntitiesStore()
const entityActions = useChronoEntityActions(entitiesStore, collectionsStore)
const savedCalendarView = userStore.getSetting('chrono.calendarView') const savedCalendarView = userStore.getSetting('chrono.calendarView')
const savedDaysViewSpan = userStore.getSetting('chrono.daysViewSpan') const savedDaysViewSpan = userStore.getSetting('chrono.daysViewSpan')
@@ -95,22 +94,6 @@ export const useChronoStore = defineStore('chronoStore', () => {
const filteredTasks = computed(() => tasks.value) const filteredTasks = computed(() => tasks.value)
function buildSources(collectionItems: CollectionObject[]): EntitySources {
return collectionItems.reduce<EntitySources>((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
}, {})
}
function setViewMode(mode: 'calendar' | 'tasks') { function setViewMode(mode: 'calendar' | 'tasks') {
viewMode.value = mode viewMode.value = mode
} }
@@ -258,17 +241,32 @@ export const useChronoStore = defineStore('chronoStore', () => {
showTaskEditor.value = false showTaskEditor.value = false
} }
function prepareEntityProperties(entity: EntityObject): ChronoEntityProperties {
const properties = entity.properties as ChronoEntityProperties
const now = new Date().toISOString()
properties.modified = now as never
if (!entity.identifier) {
properties.created = now as never
}
return properties
}
async function persistEntity(entity: EntityObject, collection: CollectionObject): Promise<EntityObject> {
const properties = prepareEntityProperties(entity).toJson()
return entity.identifier
? entitiesStore.update(entity.identifier, properties)
: entitiesStore.create(collection.identifier, properties)
}
async function toggleCalendarVisibility(collection: CollectionObject) { async function toggleCalendarVisibility(collection: CollectionObject) {
const nextVisibility = collection.properties.visibility === false const nextVisibility = collection.properties.visibility === false
const updated = collection.clone() const updated = collection.clone()
updated.properties.visibility = nextVisibility updated.properties.visibility = nextVisibility
await collectionsStore.update( await collectionsStore.update(updated.identifier, updated.properties)
updated.provider,
updated.service,
updated.identifier,
updated.properties,
)
} }
async function saveEvent(entity: EntityObject, collection?: CollectionObject | null) { async function saveEvent(entity: EntityObject, collection?: CollectionObject | null) {
@@ -280,7 +278,7 @@ export const useChronoStore = defineStore('chronoStore', () => {
throw new Error('No calendar collection selected') throw new Error('No calendar collection selected')
} }
selectedEntity.value = await entityActions.saveEntity(entity, targetCollection) selectedEntity.value = await persistEntity(entity, targetCollection)
selectedCollection.value = targetCollection selectedCollection.value = targetCollection
entityEditorMode.value = 'view' entityEditorMode.value = 'view'
} }
@@ -294,18 +292,26 @@ export const useChronoStore = defineStore('chronoStore', () => {
throw new Error('No task list selected') throw new Error('No task list selected')
} }
selectedEntity.value = await entityActions.saveEntity(entity, targetCollection) selectedEntity.value = await persistEntity(entity, targetCollection)
selectedCollection.value = targetCollection selectedCollection.value = targetCollection
entityEditorMode.value = 'view' entityEditorMode.value = 'view'
} }
async function deleteEvent(entity: EntityObject, collection?: CollectionObject | null) { async function deleteEvent(entity: EntityObject, collection?: CollectionObject | null) {
await entityActions.deleteEntity(entity, collection) if (!(collection instanceof CollectionObject) && !selectedCollection.value) {
throw new Error('No calendar collection selected')
}
await entitiesStore.delete([entity.identifier])
closeEntityEditor() closeEntityEditor()
} }
async function deleteTask(entity: EntityObject, collection?: CollectionObject | null) { async function deleteTask(entity: EntityObject, collection?: CollectionObject | null) {
await entityActions.deleteEntity(entity, collection) if (!(collection instanceof CollectionObject) && !selectedCollection.value) {
throw new Error('No task list selected')
}
await entitiesStore.delete([entity.identifier])
closeEntityEditor() closeEntityEditor()
} }
@@ -315,7 +321,15 @@ export const useChronoStore = defineStore('chronoStore', () => {
return return
} }
await entityActions.toggleTaskCompletion(taskEntity) const taskData = taskEntity.properties as TaskObject
const isCompleted = taskData.status === 'completed'
taskData.status = isCompleted ? 'needs-action' : 'completed'
taskData.completedOn = isCompleted ? null : new Date().toISOString() as never
taskData.progress = isCompleted ? null : 100
taskData.modified = new Date().toISOString() as never
await entitiesStore.update(taskEntity.identifier, taskData.toJson())
} }
async function saveCollection(collection: CollectionObject, service: ServiceObject) { async function saveCollection(collection: CollectionObject, service: ServiceObject) {
@@ -323,35 +337,35 @@ export const useChronoStore = defineStore('chronoStore', () => {
await collectionsStore.create( await collectionsStore.create(
service.provider, service.provider,
service.identifier || '', service.identifier || '',
null,
collection.properties, collection.properties,
) )
} else { } else {
await collectionsStore.update( await collectionsStore.update(collection.identifier, collection.properties)
collection.provider,
collection.service,
collection.identifier,
collection.properties,
)
} }
showCollectionEditor.value = false
} }
async function deleteCollection(collection: CollectionObject) { async function deleteCollection(collection: CollectionObject) {
await collectionsStore.delete(collection.provider, collection.service, collection.identifier) await collectionsStore.delete(collection.identifier)
if (selectedCollection.value?.identifier === collection.identifier) { if (selectedCollection.value?.identifier === collection.identifier) {
selectedCollection.value = null selectedCollection.value = null
selectedEntity.value = null
} }
showCollectionEditor.value = false
} }
async function initialize() { async function initialize() {
loading.value = true loading.value = true
try { try {
await servicesStore.list()
await collectionsStore.list() await collectionsStore.list()
const sources = buildSources(collections.value) const sources = collections.value.map(collection => collection.identifier)
if (Object.keys(sources).length > 0) { if (sources.length > 0) {
await entitiesStore.list(sources as SourceSelector) await entitiesStore.list(sources)
} }
if ( if (