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 { useServicesStore } from '@PeopleManager/stores/servicesStore'
import { CollectionObject } from '@PeopleManager/models/collection'
import { ServiceObject } from '@PeopleManager/models/service'
import type { ServiceObject } from '@PeopleManager/models/service'
// Store
const servicesStore = useServicesStore()
@@ -51,7 +51,7 @@ const dialogOpen = 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) {
@@ -61,14 +61,14 @@ const onOpen = async () => {
// Clone the collection to avoid mutating the original
editingCollection.value = props.collection.clone()
if (props.collection.id !== null) {
if (props.collection.identifier) {
// 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
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) {
return
}
editingCollection.value.color = color
editingCollection.value.properties.color = color
if (closeMenu) {
colorMenuOpen.value = false
}
@@ -132,9 +132,9 @@ watch(() => props.modelValue, async (newValue) => {
<v-combobox v-show="mode === 'create' && services.length > 1"
v-model="editingCollectionService"
label="Service"
:items="services.filter(s => s.capabilities?.CollectionCreate)"
:items="services.filter(s => s.capable('CollectionCreate'))"
item-title="label"
item-value="id"
item-value="identifier"
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>
<v-text-field
v-model="editingCollection.label"
v-model="editingCollection.properties.label"
label="Label"
required
:rules="[(v: string) => !!v || 'Label is required']"
@@ -160,7 +160,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 book color"
title="Select color"
>
@@ -192,11 +192,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"
@@ -207,7 +207,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%"
@@ -221,20 +221,10 @@ watch(() => props.modelValue, async (newValue) => {
</v-text-field>
<v-textarea
v-model="editingCollection.description"
v-model="editingCollection.properties.description"
label="Description"
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-card-text>
@@ -243,7 +233,7 @@ watch(() => props.modelValue, async (newValue) => {
<v-card-actions class="justify-space-between align-center">
<div>
<v-btn
v-if="mode === 'edit' && editingCollectionService?.capabilities?.CollectionDestroy"
v-if="mode === 'edit' && editingCollectionService?.capable('CollectionDestroy')"
color="error"
variant="text"
@click="onDelete"

View File

@@ -1,13 +1,10 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useCollectionsStore } from '@PeopleManager/stores/collectionsStore'
import { CollectionObject } from '@PeopleManager/models/collection';
// Store
const collectionsStore = useCollectionsStore()
// Props
const props = defineProps<{
collections: CollectionObject[]
loading?: boolean
selectedCollection?: CollectionObject | null
}>()
@@ -17,43 +14,14 @@ const emit = defineEmits<{
'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
const onCollectionSelect = (collection: CollectionObject) => {
console.log('[People] - Collection selected', collection)
emit('select', collection)
}
const onCollectionEdit = (collection: CollectionObject) => {
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>
<template>
@@ -73,18 +41,18 @@ defineExpose({
<v-list v-else density="compact" nav class="pa-0">
<v-list-item
v-for="collection in collections"
: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-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>
<v-list-item-title>{{ collection.label }}</v-list-item-title>
<v-list-item-title>{{ collection.properties.label || 'Unnamed' }}</v-list-item-title>
<template #append>
<v-btn

View File

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

View File

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

View File

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