Merge pull request 'refactor: standardize provider' (#3) from refactor/standardize-provider into main

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-02-25 05:20:58 +00:00
6 changed files with 369 additions and 270 deletions

View File

@@ -2,7 +2,7 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { useServicesStore } from '@PeopleManager/stores/servicesStore' import { useServicesStore } from '@PeopleManager/stores/servicesStore'
import { CollectionObject } from '@PeopleManager/models/collection' import { CollectionObject } from '@PeopleManager/models/collection'
import { ServiceObject } from '@PeopleManager/models/service' import type { ServiceObject } from '@PeopleManager/models/service'
// Store // Store
const servicesStore = useServicesStore() const servicesStore = useServicesStore()
@@ -51,7 +51,7 @@ const dialogOpen = computed({
// Functions // Functions
const onOpen = async () => { const onOpen = async () => {
if (services.value.length === 0) { if (services.value.length === 0) {
services.value = await servicesStore.list() services.value = Object.values(await servicesStore.list())
} }
if (!props.collection) { if (!props.collection) {
@@ -61,14 +61,14 @@ const onOpen = async () => {
// Clone the collection to avoid mutating the original // Clone the collection to avoid mutating the original
editingCollection.value = props.collection.clone() editingCollection.value = props.collection.clone()
if (props.collection.id !== null) { if (props.collection.identifier) {
// 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.id === props.collection!.service s.provider === props.collection!.provider && s.identifier === 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.filter(s => s.capable('CollectionCreate'))[0] || null
} }
} }
@@ -92,7 +92,7 @@ const onColorSelect = (color: string | null, closeMenu = true) => {
if (!editingCollection.value) { if (!editingCollection.value) {
return return
} }
editingCollection.value.color = color editingCollection.value.properties.color = color
if (closeMenu) { if (closeMenu) {
colorMenuOpen.value = false colorMenuOpen.value = false
} }
@@ -132,9 +132,9 @@ 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="id" item-value="identifier"
required required
:rules="[(v: ServiceObject) => !!v || 'Service is required']" :rules="[(v: ServiceObject) => !!v || 'Service is required']"
/> />
@@ -142,7 +142,7 @@ watch(() => props.modelValue, async (newValue) => {
<div class="mb-4"><strong>Service </strong> {{ editingCollection.service }}</div> <div class="mb-4"><strong>Service </strong> {{ editingCollection.service }}</div>
<v-text-field <v-text-field
v-model="editingCollection.label" v-model="editingCollection.properties.label"
label="Label" label="Label"
required required
:rules="[(v: string) => !!v || 'Label is required']" :rules="[(v: string) => !!v || 'Label is required']"
@@ -160,7 +160,7 @@ watch(() => props.modelValue, async (newValue) => {
icon icon
variant="text" variant="text"
size="small" size="small"
:style="{ color: editingCollection.color || 'var(--v-theme-on-surface)' }" :style="{ color: editingCollection.properties.color || 'var(--v-theme-on-surface)' }"
aria-label="Select book color" aria-label="Select book color"
title="Select color" title="Select color"
> >
@@ -192,11 +192,11 @@ watch(() => props.modelValue, async (newValue) => {
variant="flat" variant="flat"
size="small" size="small"
class="color-menu-body__presets--swatch" 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 }" :style="{ backgroundColor: color }"
@click="onColorSelect(color)"> @click="onColorSelect(color)">
<v-icon <v-icon
v-if="editingCollection.color === color" v-if="editingCollection.properties.color === color"
icon="mdi-check" icon="mdi-check"
size="x-small" size="x-small"
color="white" color="white"
@@ -207,7 +207,7 @@ watch(() => props.modelValue, async (newValue) => {
<div class="color-menu-body__picker"> <div class="color-menu-body__picker">
<v-color-picker <v-color-picker
v-model="editingCollection.color" v-model="editingCollection.properties.color"
mode="hex" mode="hex"
hide-canvas hide-canvas
width="100%" width="100%"
@@ -221,20 +221,10 @@ watch(() => props.modelValue, async (newValue) => {
</v-text-field> </v-text-field>
<v-textarea <v-textarea
v-model="editingCollection.description" v-model="editingCollection.properties.description"
label="Description" label="Description"
rows="2" rows="2"
/> />
<v-row>
<v-col v-if="mode === 'edit'" cols="6">
<v-switch
v-model="editingCollection.enabled"
label="Enabled"
color="primary"
/>
</v-col>
</v-row>
</v-form> </v-form>
</v-card-text> </v-card-text>
@@ -243,7 +233,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('CollectionDestroy')"
color="error" color="error"
variant="text" variant="text"
@click="onDelete" @click="onDelete"

View File

@@ -1,13 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useCollectionsStore } from '@PeopleManager/stores/collectionsStore'
import { CollectionObject } from '@PeopleManager/models/collection'; import { CollectionObject } from '@PeopleManager/models/collection';
// Store
const collectionsStore = useCollectionsStore()
// Props // Props
const props = defineProps<{ const props = defineProps<{
collections: CollectionObject[]
loading?: boolean
selectedCollection?: CollectionObject | null selectedCollection?: CollectionObject | null
}>() }>()
@@ -17,43 +14,14 @@ const emit = defineEmits<{
'edit': [collection: CollectionObject] 'edit': [collection: CollectionObject]
}>() }>()
// State
const loading = ref(false)
const collections = ref<CollectionObject[]>([])
// Lifecycle
onMounted(async () => {
loading.value = true
try {
collections.value = await collectionsStore.list()
} catch (error) {
console.error('[People] - Failed to load collections:', error)
}
loading.value = false
})
// Functions // Functions
const onCollectionSelect = (collection: CollectionObject) => { const onCollectionSelect = (collection: CollectionObject) => {
console.log('[People] - Collection selected', collection)
emit('select', collection) emit('select', collection)
} }
const onCollectionEdit = (collection: CollectionObject) => { const onCollectionEdit = (collection: CollectionObject) => {
emit('edit', collection) emit('edit', collection)
} }
// Expose refresh method
defineExpose({
async refresh() {
loading.value = true
try {
collections.value = await collectionsStore.list()
} catch (error) {
console.error('[People] - Failed to load collections:', error)
}
loading.value = false
}
})
</script> </script>
<template> <template>
@@ -73,18 +41,18 @@ defineExpose({
<v-list v-else density="compact" nav class="pa-0"> <v-list v-else density="compact" nav class="pa-0">
<v-list-item <v-list-item
v-for="collection in collections" v-for="collection in collections"
:key="collection.id" :key="collection.identifier"
:value="collection.id" :value="collection.identifier"
:active="selectedCollection?.id === collection.id" :active="selectedCollection?.identifier === collection.identifier"
@click="onCollectionSelect(collection)" @click="onCollectionSelect(collection)"
rounded="lg" rounded="lg"
class="mb-1" class="mb-1"
> >
<template #prepend> <template #prepend>
<v-icon :color="collection.color || 'primary'" icon="mdi-book-outline" size="small" /> <v-icon :color="collection.properties.color || 'primary'" icon="mdi-book-outline" size="small" />
</template> </template>
<v-list-item-title>{{ collection.label }}</v-list-item-title> <v-list-item-title>{{ collection.properties.label || 'Unnamed' }}</v-list-item-title>
<template #append> <template #append>
<v-btn <v-btn

View File

@@ -37,9 +37,9 @@ const saving = ref(false)
// Computed // Computed
const mode = computed(() => props.mode) const mode = computed(() => props.mode)
const entity = computed(() => props.selectedEntity || null) const entity = computed(() => props.selectedEntity || null)
const entityObject = computed(() => entity.value?.data ?? null) const entityObject = computed(() => entity.value?.properties ?? null)
const entityFresh = computed(() => entity.value?.id === null || entity.value?.id === undefined) const entityFresh = computed(() => !entity.value?.identifier)
const entityType = computed(() => entityObject.value?.type || 'individual') const entityType = computed(() => (entityObject.value as any)?.type || 'individual')
// Determine which sections to show based on entity type // Determine which sections to show based on entity type
const showNames = computed(() => entityType.value === 'individual') const showNames = computed(() => entityType.value === 'individual')
@@ -194,7 +194,7 @@ const removeVirtualLocation = (key: string) => {
<div class="people-editor-header d-flex justify-space-between pa-4"> <div class="people-editor-header d-flex justify-space-between pa-4">
<div> <div>
<v-icon :icon="entityIcon" class="mr-2" /> <v-icon :icon="entityIcon" class="mr-2" />
{{ entity?.data?.label || 'Nothing Selected' }} {{ entity?.properties?.label || 'Nothing Selected' }}
</div> </div>
<div v-if="!loading && entity"> <div v-if="!loading && entity">
@@ -236,20 +236,20 @@ const removeVirtualLocation = (key: string) => {
<PersonEditorLabel <PersonEditorLabel
v-if="showLabel" v-if="showLabel"
:mode="mode" :mode="mode"
:label="entity.data!.label" :label="(entityObject as any)?.label"
@update:label="entity.data!.label = $event" @update:label="(entityObject as any).label = $event"
/> />
<PersonEditorNames <PersonEditorNames
v-if="showNames" v-if="showNames"
:mode="mode" :mode="mode"
:names="entity.data!.names" :names="(entityObject as any)?.names"
@update:names="entity.data!.names = $event" @update:names="(entityObject as any).names = $event"
/> />
<PersonEditorTitles <PersonEditorTitles
v-if="showTitles" v-if="showTitles"
:titles="(entity.data as any)!.titles || {}" :titles="(entityObject as any)?.titles || {}"
:mode="mode" :mode="mode"
@add-title="addTitle" @add-title="addTitle"
@remove-title="removeTitle" @remove-title="removeTitle"
@@ -257,7 +257,7 @@ const removeVirtualLocation = (key: string) => {
<PersonEditorAnniversaries <PersonEditorAnniversaries
v-if="showAnniversaries" v-if="showAnniversaries"
:anniversaries="(entity.data as any)!.anniversaries || []" :anniversaries="(entityObject as any)?.anniversaries || []"
:mode="mode" :mode="mode"
@add-anniversary="addAnniversary" @add-anniversary="addAnniversary"
@remove-anniversary="removeAnniversary" @remove-anniversary="removeAnniversary"
@@ -266,7 +266,7 @@ const removeVirtualLocation = (key: string) => {
<PersonEditorPhysicalLocations <PersonEditorPhysicalLocations
v-if="showPhysicalLocations" v-if="showPhysicalLocations"
:physical-locations="(entity.data as any)!.physicalLocations || {}" :physical-locations="(entityObject as any)?.physicalLocations || {}"
:mode="mode" :mode="mode"
@add-physical-location="addPhysicalLocation" @add-physical-location="addPhysicalLocation"
@remove-physical-location="removePhysicalLocation" @remove-physical-location="removePhysicalLocation"
@@ -274,7 +274,7 @@ const removeVirtualLocation = (key: string) => {
<PersonEditorPhones <PersonEditorPhones
v-if="showPhones" v-if="showPhones"
:phones="(entity.data as any)!.phones || {}" :phones="(entityObject as any)?.phones || {}"
:mode="mode" :mode="mode"
@add-phone="addPhone" @add-phone="addPhone"
@remove-phone="removePhone" @remove-phone="removePhone"
@@ -282,7 +282,7 @@ const removeVirtualLocation = (key: string) => {
<PersonEditorEmails <PersonEditorEmails
v-if="showEmails" v-if="showEmails"
:emails="(entity.data as any)!.emails || {}" :emails="(entityObject as any)?.emails || {}"
:mode="mode" :mode="mode"
@add-email="addEmail" @add-email="addEmail"
@remove-email="removeEmail" @remove-email="removeEmail"
@@ -290,7 +290,7 @@ const removeVirtualLocation = (key: string) => {
<PersonEditorVirtualLocations <PersonEditorVirtualLocations
v-if="showVirtualLocations" v-if="showVirtualLocations"
:virtual-locations="(entity.data as any)!.virtualLocations || {}" :virtual-locations="(entityObject as any)?.virtualLocations || {}"
:mode="mode" :mode="mode"
@add-virtual-location="addVirtualLocation" @add-virtual-location="addVirtualLocation"
@remove-virtual-location="removeVirtualLocation" @remove-virtual-location="removeVirtualLocation"
@@ -298,7 +298,7 @@ const removeVirtualLocation = (key: string) => {
<PersonEditorOrganizations <PersonEditorOrganizations
v-if="showOrganizations" v-if="showOrganizations"
:organizations="(entity.data as any)!.organizations || {}" :organizations="(entityObject as any)?.organizations || {}"
:mode="mode" :mode="mode"
@add-organization="addOrganization" @add-organization="addOrganization"
@remove-organization="removeOrganization" @remove-organization="removeOrganization"
@@ -306,7 +306,7 @@ const removeVirtualLocation = (key: string) => {
<PersonEditorMembers <PersonEditorMembers
v-if="showMembers" v-if="showMembers"
:members="(entity.data as any)!.members || {}" :members="(entityObject as any)?.members || {}"
:mode="mode" :mode="mode"
@add-member="addMember" @add-member="addMember"
@remove-member="removeMember" @remove-member="removeMember"
@@ -314,7 +314,7 @@ const removeVirtualLocation = (key: string) => {
<PersonEditorNotes <PersonEditorNotes
v-if="showNotes" v-if="showNotes"
:notes="entity.data!.notes || {}" :notes="(entityObject as any)?.notes || {}"
:mode="mode" :mode="mode"
@add-note="addNote" @add-note="addNote"
@remove-note="removeNote" @remove-note="removeNote"
@@ -322,7 +322,7 @@ const removeVirtualLocation = (key: string) => {
<PersonEditorLanguages <PersonEditorLanguages
v-if="showLanguages" v-if="showLanguages"
:languages="(entity.data as any)!.languages || []" :languages="(entityObject as any)?.languages || []"
:mode="mode" :mode="mode"
@add-language="addLanguage" @add-language="addLanguage"
@remove-language="removeLanguage" @remove-language="removeLanguage"
@@ -330,7 +330,7 @@ const removeVirtualLocation = (key: string) => {
<PersonEditorMedia <PersonEditorMedia
v-if="showMedia" v-if="showMedia"
:media="entity.data!.media || {}" :media="(entityObject as any)?.media || {}"
:mode="mode" :mode="mode"
@add-media="addMedia" @add-media="addMedia"
@remove-media="removeMedia" @remove-media="removeMedia"

View File

@@ -31,8 +31,15 @@ const filteredEntities = computed(() => {
} }
const query = searchQuery.value.toLowerCase() const query = searchQuery.value.toLowerCase()
return collectionEntities.value.filter(entity => { return collectionEntities.value.filter(entity => {
const name = `${entity.data?.names.family || ''} ${entity.data?.names.given || ''}`.toLowerCase() const props = (entity.properties as any)
const email = (entity.data?.emails?.[0]?.address || '').toLowerCase() const given = props?.names?.given || ''
const family = props?.names?.family || ''
const full = props?.names?.full || ''
const label = props?.label || ''
const name = `${given} ${family} ${full} ${label}`.toLowerCase()
const emails = props?.emails
const firstEmail = emails ? Object.values(emails)[0] as any : null
const email = (firstEmail?.address || '').toLowerCase()
return name.includes(query) || email.includes(query) return name.includes(query) || email.includes(query)
}) })
}) })
@@ -42,13 +49,17 @@ watch(() => props.selectedCollection, async (newCollection) => {
if (newCollection) { if (newCollection) {
loading.value = true loading.value = true
try { try {
collectionEntities.value = await entitiesStore.list( const sources = {
newCollection.provider, [newCollection.provider]: {
newCollection.service, [String(newCollection.service)]: {
newCollection.id [String(newCollection.identifier)]: true as const,
); },
},
}
const result = await entitiesStore.list(sources)
collectionEntities.value = Object.values(result)
} catch (error) { } catch (error) {
console.error('[People] - Failed to load contacts:', error); console.error('[People] - Failed to load contacts:', error)
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -56,11 +67,11 @@ watch(() => props.selectedCollection, async (newCollection) => {
collectionEntities.value = [] collectionEntities.value = []
loading.value = false loading.value = false
} }
}, { immediate: true }); }, { immediate: true })
// Functions // Functions
function entityType(entity: EntityObject): string { function entityType(entity: EntityObject): string {
return entity.data?.type || 'individual' return (entity.properties as any)?.type || 'individual'
} }
function entityIcon(entity: EntityObject): string { function entityIcon(entity: EntityObject): string {
@@ -76,62 +87,65 @@ function entityIcon(entity: EntityObject): string {
} }
function entityPhoto(entity: EntityObject): string | null { function entityPhoto(entity: EntityObject): string | null {
// TODO: Implement photo retrieval from entity data // TODO: Implement photo retrieval from entity properties
return null return null
} }
function entityInitials(entity: EntityObject): string { function entityInitials(entity: EntityObject): string {
const type = entityType(entity) const type = entityType(entity)
const props = (entity.properties as any)
if (type === 'organization') { if (type === 'organization') {
const name = (entity.data as any)?.names?.full || entity.data?.label || '' const name = props?.names?.full || props?.label || ''
return name.substring(0, 2).toUpperCase() || 'OR' return name.substring(0, 2).toUpperCase() || 'OR'
} }
if (type === 'group') { if (type === 'group') {
const name = (entity.data as any)?.names?.full || entity.data?.label || '' const name = props?.names?.full || props?.label || ''
return name.substring(0, 2).toUpperCase() || 'GR' return name.substring(0, 2).toUpperCase() || 'GR'
} }
// Individual // Individual
const given = (entity.data as any)?.names?.given const given = props?.names?.given
const family = (entity.data as any)?.names?.family const family = props?.names?.family
const initials = `${given?.[0] || ''}${family?.[0] || ''}`.toUpperCase() const initials = `${given?.[0] || ''}${family?.[0] || ''}`.toUpperCase()
return initials || '?' return initials || '?'
} }
function entityLabel(entity: EntityObject): string { function entityLabel(entity: EntityObject): string {
const type = entityType(entity) const type = entityType(entity)
const props = (entity.properties as any)
if (type === 'organization' || type === 'group') { if (type === 'organization' || type === 'group') {
return entity.data?.label || (entity.data as any)?.names?.full || 'Unknown' return props?.label || props?.names?.full || 'Unknown'
} }
// Individual // Individual
return entity.data?.label || `${(entity.data as any)?.names?.given || ''} ${(entity.data as any)?.names?.family || ''}`.trim() || 'Unknown' const given = props?.names?.given || ''
const family = props?.names?.family || ''
return props?.label || `${given} ${family}`.trim() || 'Unknown'
} }
function entityEmail(entity: EntityObject): string | null { function entityEmail(entity: EntityObject): string | null {
const emails = (entity.data as any)?.emails const emails = (entity.properties as any)?.emails
if (!emails) return null if (!emails) return null
const emailEntries = Object.values(emails) const emailEntries = Object.values(emails) as any[]
if (emailEntries.length === 0) return null if (emailEntries.length === 0) return null
// Sort by priority (assuming lower number = higher priority) emailEntries.sort((a, b) => (a.priority || 999) - (b.priority || 999))
emailEntries.sort((a: any, b: any) => (a.priority || 999) - (b.priority || 999)) return emailEntries[0].address || null
return (emailEntries[0] as any).address || null
} }
function entityOrganization(entity: EntityObject): string | null { function entityOrganization(entity: EntityObject): string | null {
const organizations = (entity.data as any)?.organizations const organizations = (entity.properties as any)?.organizations
if (!organizations || Object.keys(organizations).length === 0) return null if (!organizations || Object.keys(organizations).length === 0) return null
const orgEntries = Object.values(organizations) const orgEntries = Object.values(organizations) as any[]
return (orgEntries[0] as any).Label || null return orgEntries[0].label || null
} }
function entityMemberCount(entity: EntityObject): number | null { function entityMemberCount(entity: EntityObject): number | null {
const type = entityType(entity) const type = entityType(entity)
if (type !== 'group') return null if (type !== 'group') return null
const members = (entity.data as any)?.members const members = (entity.properties as any)?.members
if (!members) return 0 if (!members) return 0
return Object.keys(members).length return Object.keys(members).length
} }
@@ -179,8 +193,8 @@ function entityAvatarColor(entity: EntityObject): string {
<v-list v-else density="compact" nav> <v-list v-else density="compact" nav>
<v-list-item <v-list-item
v-for="entity in filteredEntities" v-for="entity in filteredEntities"
:key="entity.id" :key="entity.identifier"
:value="entity.id" :value="entity.identifier"
@click="$emit('select', entity)"> @click="$emit('select', entity)">
<template #prepend> <template #prepend>

View File

@@ -1,12 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { useModuleStore } from '@KTXC/stores/moduleStore' import { useModuleStore } from '@KTXC/stores/moduleStore'
import { useCollectionsStore } from '@PeopleManager/stores/collectionsStore' import { usePeopleStore } from '@/stores/peopleStore'
import { useEntitiesStore } from '@PeopleManager/stores/entitiesStore'
import { ServiceObject } from '@PeopleManager/models/service';
import { CollectionObject } from '@PeopleManager/models/collection';
import { EntityObject } from '@PeopleManager/models/entity';
import CollectionList from '@/components/CollectionList.vue' import CollectionList from '@/components/CollectionList.vue'
import CollectionEditor from '@/components/CollectionEditor.vue' import CollectionEditor from '@/components/CollectionEditor.vue'
import PersonList from '@/components/PersonList.vue' import PersonList from '@/components/PersonList.vue'
@@ -15,148 +12,50 @@ import PersonEditor from '@/components/PersonEditor.vue'
// Vuetify display // Vuetify display
const display = useDisplay() const display = useDisplay()
// View state
const sidebarVisible = ref(true)
// Check if people manager is available // Check if people manager is available
const moduleStore = useModuleStore() const moduleStore = useModuleStore()
const isPeopleManagerAvailable = computed(() => { const isPeopleManagerAvailable = computed(() => {
return moduleStore.has('people_manager') || moduleStore.has('PeopleManager') return moduleStore.has('people_manager') || moduleStore.has('PeopleManager')
}) })
// Local state for UI selections // Store
const collectionsStore = useCollectionsStore() const peopleStore = usePeopleStore()
const entitiesStore = useEntitiesStore()
const selectedCollection = ref<CollectionObject | null>(null)
const selectedEntity = ref<EntityObject | null>(null)
const entityEditorMode = ref<'edit' | 'view'>('view')
// Collection editor state const {
const showCollectionEditor = ref(false) sidebarVisible,
const editingCollection = ref<CollectionObject | null>(null) loading,
const collectionEditorMode = ref<'create' | 'edit'>('create') selectedCollection,
const CollectionListRef = ref<InstanceType<typeof CollectionList> | null>(null) selectedEntity,
entityEditorMode,
showCollectionEditor,
editingCollection,
collectionEditorMode,
collections,
} = storeToRefs(peopleStore)
// Handlers const {
const handleEditorEdit = () => { initialize,
console.log('[People] - Editor editing started') selectCollection,
entityEditorMode.value = 'edit' openEditCollection,
} openCreateCollection,
selectEntity,
createEntity,
startEditingEntity,
cancelEditingEntity,
closeEntityEditor,
saveEntity,
deleteEntity,
saveCollection,
deleteCollection,
} = peopleStore
const handleEditorCancel = () => { onMounted(async () => {
console.log('[People] - Editor editing cancelled') try {
entityEditorMode.value = 'view' await initialize()
} } catch (error) {
console.error('[People] - Failed to load data from PeopleManager:', error)
const handleEditorClose = () => {
console.log('[People] - Editor closed')
selectedEntity.value = null
entityEditorMode.value = 'view'
}
const handleCollectionSelect = (collection: CollectionObject) => {
console.log('[People] - Collection selected', collection)
selectedCollection.value = collection ?? null
selectedEntity.value = null
}
const handleCollectionEdit = (collection: CollectionObject) => {
console.log('[People] - Collection edit', collection)
editingCollection.value = collection
collectionEditorMode.value = 'edit'
showCollectionEditor.value = true
}
const onCollectionFresh = () => {
console.log('[People] - Collection new')
editingCollection.value = collectionsStore.fresh()
collectionEditorMode.value = 'create'
showCollectionEditor.value = true
}
const handleCollectionSave = async (collection: CollectionObject, service: ServiceObject) => {
console.log('[People] - Collection save', collection)
if (collectionEditorMode.value === 'create') {
await handleCollectionCreate(collection, service)
} else {
await handleCollectionModify(collection)
}
await CollectionListRef.value?.refresh()
}
const handleCollectionCreate = async (collection: CollectionObject, service: ServiceObject) => {
console.log('[People] - Collection created', collection)
const result = await collectionsStore.create(service, collection)
if (result instanceof CollectionObject) {
selectedCollection.value = result
} else {
selectedCollection.value = null
}
selectedEntity.value = null
}
const handleCollectionModify = async (collection: CollectionObject) => {
console.log('[People] - Collection modified', collection)
await collectionsStore.modify(collection)
}
const handleCollectionDelete = async (collection: CollectionObject) => {
console.log('[People] - Collection deleted', collection)
await collectionsStore.destroy(collection)
selectedCollection.value = null
selectedEntity.value = null
await CollectionListRef.value?.refresh()
}
const handleEntitySelect = (entity: EntityObject) => {
console.log('[People] - Entity selected', entity)
selectedEntity.value = entity
entityEditorMode.value = 'view'
}
const onEntityFresh = (type: string) => {
console.log('[People] - Entity create', type)
selectedEntity.value = entitiesStore.fresh(type)
entityEditorMode.value = 'edit'
}
const handleEntitySave = async (entity: EntityObject, collection?: CollectionObject | null) => {
console.log('[People] - Entity save', entity)
if (!(collection instanceof CollectionObject)) {
collection = selectedCollection.value
}
if (!collection) {
console.warn('[People] - Invalid collection object cannot save entity')
return
}
if (entity.id === null) {
console.log('[People] - Entity create', entity)
entity.data.created = new Date();
selectedEntity.value = await entitiesStore.create(collection, entity)
} else if (entity.id !== null) {
console.log('[People] - Entity modify', entity)
entity.data.modified = new Date();
selectedEntity.value = await entitiesStore.modify(collection, entity)
}
entityEditorMode.value = 'view'
}
const handleEntityDelete = async (entity: EntityObject, collection?: CollectionObject | null) => {
console.log('[People] - Entity deleted', entity)
if (!(collection instanceof CollectionObject)) {
collection = selectedCollection.value
}
if (!collection) {
console.warn('[People] - Invalid collection object cannot delete entity')
return
}
await entitiesStore.destroy(collection, entity)
selectedEntity.value = null
entityEditorMode.value = 'view'
} }
})
</script> </script>
<template> <template>
@@ -191,10 +90,11 @@ const handleEntityDelete = async (entity: EntityObject, collection?: CollectionO
> >
<div class="pa-4 d-flex flex-column h-100"> <div class="pa-4 d-flex flex-column h-100">
<CollectionList <CollectionList
ref="CollectionListRef" :collections="collections"
:loading="loading"
:selected-collection="selectedCollection" :selected-collection="selectedCollection"
@select="handleCollectionSelect" @select="selectCollection"
@edit="handleCollectionEdit" @edit="openEditCollection"
/> />
<v-divider class="my-2" /> <v-divider class="my-2" />
@@ -211,7 +111,7 @@ const handleEntityDelete = async (entity: EntityObject, collection?: CollectionO
block block
class="justify-start" class="justify-start"
:disabled="!selectedCollection" :disabled="!selectedCollection"
@click="onEntityFresh('individual')" @click="createEntity('individual')"
> >
Individual Individual
</v-btn> </v-btn>
@@ -222,7 +122,7 @@ const handleEntityDelete = async (entity: EntityObject, collection?: CollectionO
block block
class="justify-start" class="justify-start"
:disabled="!selectedCollection" :disabled="!selectedCollection"
@click="onEntityFresh('organization')" @click="createEntity('organization')"
> >
Organization Organization
</v-btn> </v-btn>
@@ -233,7 +133,7 @@ const handleEntityDelete = async (entity: EntityObject, collection?: CollectionO
block block
class="justify-start" class="justify-start"
:disabled="!selectedCollection" :disabled="!selectedCollection"
@click="onEntityFresh('group')" @click="createEntity('group')"
> >
Group Group
</v-btn> </v-btn>
@@ -243,7 +143,7 @@ const handleEntityDelete = async (entity: EntityObject, collection?: CollectionO
size="small" size="small"
block block
class="justify-start" class="justify-start"
@click="onCollectionFresh" @click="openCreateCollection"
> >
Address Book Address Book
</v-btn> </v-btn>
@@ -281,8 +181,8 @@ const handleEntityDelete = async (entity: EntityObject, collection?: CollectionO
<PersonList <PersonList
:selected-collection="selectedCollection" :selected-collection="selectedCollection"
:selected-entity="selectedEntity" :selected-entity="selectedEntity"
@select="handleEntitySelect" @select="selectEntity"
@fresh="onEntityFresh" @fresh="createEntity"
/> />
</div> </div>
<div class="people-editor-panel"> <div class="people-editor-panel">
@@ -290,11 +190,11 @@ const handleEntityDelete = async (entity: EntityObject, collection?: CollectionO
:mode="entityEditorMode" :mode="entityEditorMode"
:selected-collection="selectedCollection" :selected-collection="selectedCollection"
:selected-entity="selectedEntity" :selected-entity="selectedEntity"
@save="handleEntitySave" @save="saveEntity"
@delete="handleEntityDelete" @delete="deleteEntity"
@edit="handleEditorEdit" @edit="startEditingEntity"
@cancel="handleEditorCancel" @cancel="cancelEditingEntity"
@close="handleEditorClose" @close="closeEntityEditor"
/> />
</div> </div>
</div> </div>
@@ -306,8 +206,8 @@ const handleEntityDelete = async (entity: EntityObject, collection?: CollectionO
v-model="showCollectionEditor" v-model="showCollectionEditor"
:collection="editingCollection" :collection="editingCollection"
:mode="collectionEditorMode" :mode="collectionEditorMode"
@save="handleCollectionSave" @save="saveCollection"
@delete="handleCollectionDelete" @delete="deleteCollection"
/> />
</div> </div>
</template> </template>

227
src/stores/peopleStore.ts Normal file
View File

@@ -0,0 +1,227 @@
import { computed, ref, shallowRef } from 'vue'
import { defineStore } from 'pinia'
import { CollectionObject } from '@PeopleManager/models/collection'
import { EntityObject } from '@PeopleManager/models/entity'
import { IndividualObject } from '@PeopleManager/models/individual'
import { OrganizationObject } from '@PeopleManager/models/organization'
import { GroupObject } from '@PeopleManager/models/group'
import type { ServiceObject } from '@PeopleManager/models/service'
import { useCollectionsStore } from '@PeopleManager/stores/collectionsStore'
import { useEntitiesStore } from '@PeopleManager/stores/entitiesStore'
export const usePeopleStore = defineStore('peopleStore', () => {
const collectionsStore = useCollectionsStore()
const entitiesStore = useEntitiesStore()
// UI state
const sidebarVisible = ref(true)
const loading = ref(false)
// Selection state
const selectedCollection = shallowRef<CollectionObject | null>(null)
const selectedEntity = shallowRef<EntityObject | null>(null)
// Entity editor state
const entityEditorMode = ref<'edit' | 'view'>('view')
// Collection editor state
const showCollectionEditor = ref(false)
const editingCollection = shallowRef<CollectionObject | null>(null)
const collectionEditorMode = ref<'create' | 'edit'>('create')
// Derived state
const collections = computed(() => collectionsStore.collections)
const entities = computed(() => entitiesStore.entities)
// --- Collection actions ---
function selectCollection(collection: CollectionObject) {
selectedCollection.value = collection
selectedEntity.value = null
}
function openEditCollection(collection: CollectionObject) {
editingCollection.value = collection
collectionEditorMode.value = 'edit'
showCollectionEditor.value = true
}
function openCreateCollection() {
editingCollection.value = new CollectionObject()
collectionEditorMode.value = 'create'
showCollectionEditor.value = true
}
async function saveCollection(collection: CollectionObject, service: ServiceObject) {
if (collectionEditorMode.value === 'create') {
const created = await collectionsStore.create(
service.provider,
service.identifier || '',
null,
collection.properties,
)
selectedCollection.value = created
} else {
await collectionsStore.update(
collection.provider,
collection.service,
collection.identifier,
collection.properties,
)
}
showCollectionEditor.value = false
}
async function deleteCollection(collection: CollectionObject) {
await collectionsStore.delete(collection.provider, collection.service, collection.identifier)
if (selectedCollection.value?.identifier === collection.identifier) {
selectedCollection.value = null
selectedEntity.value = null
}
showCollectionEditor.value = false
}
// --- Entity actions ---
function createEntity(type: string) {
const entity = new EntityObject()
if (type === 'organization') {
entity.properties = new OrganizationObject()
} else if (type === 'group') {
entity.properties = new GroupObject()
} else {
entity.properties = new IndividualObject()
}
selectedEntity.value = entity
entityEditorMode.value = 'edit'
}
function selectEntity(entity: EntityObject) {
selectedEntity.value = entity
entityEditorMode.value = 'view'
}
function startEditingEntity() {
entityEditorMode.value = 'edit'
}
function cancelEditingEntity() {
entityEditorMode.value = 'view'
}
function closeEntityEditor() {
selectedEntity.value = null
entityEditorMode.value = 'view'
}
async function saveEntity(entity: EntityObject, collection?: CollectionObject | null) {
const targetCollection = collection instanceof CollectionObject
? collection
: selectedCollection.value
if (!targetCollection) {
throw new Error('[People] - No collection selected, cannot save entity')
}
if (!entity.identifier) {
// Create
entity.properties.created = new Date()
selectedEntity.value = await entitiesStore.create(
targetCollection.provider,
targetCollection.service,
targetCollection.identifier,
entity.properties,
)
} else {
// Update
entity.properties.modified = new Date()
selectedEntity.value = await entitiesStore.update(
targetCollection.provider,
targetCollection.service,
targetCollection.identifier,
entity.identifier,
entity.properties,
)
}
selectedCollection.value = targetCollection
entityEditorMode.value = 'view'
}
async function deleteEntity(entity: EntityObject, collection?: CollectionObject | null) {
const targetCollection = collection instanceof CollectionObject
? collection
: selectedCollection.value
if (!targetCollection) {
throw new Error('[People] - No collection selected, cannot delete entity')
}
await entitiesStore.delete(
targetCollection.provider,
targetCollection.service,
targetCollection.identifier,
entity.identifier,
)
closeEntityEditor()
}
// --- Lifecycle ---
async function initialize() {
loading.value = true
try {
await collectionsStore.list()
if (
selectedCollection.value
&& !collections.value.find(c => c.identifier === selectedCollection.value?.identifier)
) {
selectedCollection.value = null
}
} finally {
loading.value = false
}
}
return {
// State
sidebarVisible,
loading,
selectedCollection,
selectedEntity,
entityEditorMode,
showCollectionEditor,
editingCollection,
collectionEditorMode,
// Derived
collections,
entities,
// Collection actions
selectCollection,
openEditCollection,
openCreateCollection,
saveCollection,
deleteCollection,
// Entity actions
createEntity,
selectEntity,
startEditingEntity,
cancelEditingEntity,
closeEntityEditor,
saveEntity,
deleteEntity,
// Lifecycle
initialize,
}
})