refactor: use unified manager design
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user