From c7309ac55348a9b8f035608c59554ff5a8b2693d Mon Sep 17 00:00:00 2001 From: Alexander Ackermann Date: Mon, 16 Mar 2026 20:21:59 +0100 Subject: [PATCH 1/7] feat: add contacts app skeleton # Conflicts: # pnpm-lock.yaml --- dev/docker/opencloud.web.config.json | 3 +- packages/web-app-contacts/l10n/.tx/config | 10 + .../web-app-contacts/l10n/translations.json | 1 + packages/web-app-contacts/package.json | 21 +++ .../web-app-contacts/src/LayoutContainer.vue | 7 + packages/web-app-contacts/src/appid.ts | 1 + .../src/components/AddressBooksList.vue | 77 ++++++++ .../composables/piniaStores/addressbooks.ts | 78 ++++++++ .../src/composables/piniaStores/contacts.ts | 78 ++++++++ .../src/composables/useLoadAddressbooks.ts | 42 +++++ .../src/composables/useLoadContacts.ts | 46 +++++ packages/web-app-contacts/src/extensions.ts | 71 +++++++ packages/web-app-contacts/src/index.ts | 51 ++++++ packages/web-app-contacts/src/types.ts | 173 ++++++++++++++++++ .../web-app-contacts/src/views/Contacts.vue | 69 +++++++ .../src/components/MailboxTree.vue | 93 +++++----- 16 files changed, 768 insertions(+), 53 deletions(-) create mode 100644 packages/web-app-contacts/l10n/.tx/config create mode 100644 packages/web-app-contacts/l10n/translations.json create mode 100644 packages/web-app-contacts/package.json create mode 100644 packages/web-app-contacts/src/LayoutContainer.vue create mode 100644 packages/web-app-contacts/src/appid.ts create mode 100644 packages/web-app-contacts/src/components/AddressBooksList.vue create mode 100644 packages/web-app-contacts/src/composables/piniaStores/addressbooks.ts create mode 100644 packages/web-app-contacts/src/composables/piniaStores/contacts.ts create mode 100644 packages/web-app-contacts/src/composables/useLoadAddressbooks.ts create mode 100644 packages/web-app-contacts/src/composables/useLoadContacts.ts create mode 100644 packages/web-app-contacts/src/extensions.ts create mode 100644 packages/web-app-contacts/src/index.ts create mode 100644 packages/web-app-contacts/src/types.ts create mode 100644 packages/web-app-contacts/src/views/Contacts.vue diff --git a/dev/docker/opencloud.web.config.json b/dev/docker/opencloud.web.config.json index a9bc525fdc..3838f4896d 100644 --- a/dev/docker/opencloud.web.config.json +++ b/dev/docker/opencloud.web.config.json @@ -17,6 +17,7 @@ "app-store", "activities", "preview", - "mail" + "mail", + "contacts" ] } diff --git a/packages/web-app-contacts/l10n/.tx/config b/packages/web-app-contacts/l10n/.tx/config new file mode 100644 index 0000000000..1a50e777d4 --- /dev/null +++ b/packages/web-app-contacts/l10n/.tx/config @@ -0,0 +1,10 @@ +[main] +host = https://www.transifex.com + +[o:opencloud-eu:p:opencloud-eu:r:web-contacts] +file_filter = locale//app.po +minimum_perc = 0 +resource_name = web-contacts +source_file = template.pot +source_lang = en +type = PO diff --git a/packages/web-app-contacts/l10n/translations.json b/packages/web-app-contacts/l10n/translations.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/web-app-contacts/l10n/translations.json @@ -0,0 +1 @@ +{} diff --git a/packages/web-app-contacts/package.json b/packages/web-app-contacts/package.json new file mode 100644 index 0000000000..b6bb760328 --- /dev/null +++ b/packages/web-app-contacts/package.json @@ -0,0 +1,21 @@ +{ + "name": "web-app-contacts", + "version": "0.0.0", + "private": true, + "description": "OpenCloud groupware contacts app", + "license": "AGPL-3.0", + "devDependencies": { + "@opencloud-eu/web-test-helpers": "workspace:*" + }, + "peerDependencies": { + "@opencloud-eu/design-system": "workspace:^", + "@opencloud-eu/web-client": "workspace:*", + "@opencloud-eu/web-pkg": "workspace:*", + "lodash-es": "4.17.23", + "pinia": "3.0.4", + "vue-concurrency": "^0.3.0", + "vue-router": "^4.2.5 || ^5.0.0", + "vue3-gettext": "^4.0.0-beta.1", + "zod": "^3.24.1" + } +} diff --git a/packages/web-app-contacts/src/LayoutContainer.vue b/packages/web-app-contacts/src/LayoutContainer.vue new file mode 100644 index 0000000000..db60eb7292 --- /dev/null +++ b/packages/web-app-contacts/src/LayoutContainer.vue @@ -0,0 +1,7 @@ + + + diff --git a/packages/web-app-contacts/src/appid.ts b/packages/web-app-contacts/src/appid.ts new file mode 100644 index 0000000000..e18548961c --- /dev/null +++ b/packages/web-app-contacts/src/appid.ts @@ -0,0 +1 @@ +export const APPID = 'contacts' diff --git a/packages/web-app-contacts/src/components/AddressBooksList.vue b/packages/web-app-contacts/src/components/AddressBooksList.vue new file mode 100644 index 0000000000..6058cbba5f --- /dev/null +++ b/packages/web-app-contacts/src/components/AddressBooksList.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/packages/web-app-contacts/src/composables/piniaStores/addressbooks.ts b/packages/web-app-contacts/src/composables/piniaStores/addressbooks.ts new file mode 100644 index 0000000000..acf765230e --- /dev/null +++ b/packages/web-app-contacts/src/composables/piniaStores/addressbooks.ts @@ -0,0 +1,78 @@ +import { defineStore } from 'pinia' +import { computed, ref, unref } from 'vue' +import { AddressBook } from '../../types' +import { useRouteQuery } from '@opencloud-eu/web-pkg/src' + +export const useAddressBooksStore = defineStore('addressBooks', () => { + const currentAddressBookIdQuery = useRouteQuery('addressBookId') + + const addressBooks = ref([]) + const currentAddressBookId = ref() + + const currentAddressBook = computed(() => + unref(addressBooks).find((addressBook) => addressBook.id === unref(currentAddressBookId)) + ) + + const setAddressBooks = (data: AddressBook[]) => { + addressBooks.value = data + } + + const upsertAddressBook = (data: AddressBook) => { + const existing = unref(addressBooks).find(({ id }) => id === data.id) + if (existing) { + Object.assign(existing, data) + return + } + unref(addressBooks).push(data) + } + + const removeAddressBooks = (values: AddressBook[]) => { + addressBooks.value = unref(addressBooks).filter( + (addressBook) => !values.find(({ id }) => id === addressBook.id) + ) + + if (values.some((v) => v.id === unref(currentAddressBookId))) { + currentAddressBookId.value = null + currentAddressBookIdQuery.value = null + } + } + + const setCurrentAddressBook = (data: AddressBook) => { + currentAddressBookId.value = data?.id + currentAddressBookIdQuery.value = data?.id + } + + const updateAddressBookField = ({ + id, + field, + value + }: { + id: T['id'] + field: keyof T + value: T[keyof T] + }) => { + const addressBook = unref(addressBooks).find((addressBook) => id === addressBook.id) as T + if (addressBook) { + addressBook[field] = value + } + } + + const reset = () => { + addressBooks.value = [] + currentAddressBookId.value = null + currentAddressBookIdQuery.value = null + } + + return { + addressBooks, + currentAddressBook, + updateAddressBookField, + setAddressBooks, + upsertAddressBook, + removeAddressBooks, + setCurrentAddressBook, + reset + } +}) + +export type AddressBooksStore = ReturnType diff --git a/packages/web-app-contacts/src/composables/piniaStores/contacts.ts b/packages/web-app-contacts/src/composables/piniaStores/contacts.ts new file mode 100644 index 0000000000..872513d70d --- /dev/null +++ b/packages/web-app-contacts/src/composables/piniaStores/contacts.ts @@ -0,0 +1,78 @@ +import { defineStore } from 'pinia' +import { computed, ref, unref } from 'vue' +import { Contact } from '../../types' +import { useRouteQuery } from '@opencloud-eu/web-pkg/src' + +export const useContactsStore = defineStore('contacts', () => { + const currentContactIdQuery = useRouteQuery('contactId') + + const contacts = ref([]) + const currentContactId = ref() + + const currentContact = computed(() => + unref(contacts).find((contact) => contact.id === unref(currentContactId)) + ) + + const setContacts = (data: Contact[]) => { + contacts.value = data + } + + const upsertContact = (data: Contact) => { + const existing = unref(contacts).find(({ id }) => id === data.id) + if (existing) { + Object.assign(existing, data) + return + } + unref(contacts).push(data) + } + + const removeContacts = (values: Contact[]) => { + contacts.value = unref(contacts).filter( + (contact) => !values.find(({ id }) => id === contact.id) + ) + + if (values.some((v) => v.id === unref(currentContactId))) { + currentContactId.value = null + currentContactIdQuery.value = null + } + } + + const setCurrentContact = (data: Contact) => { + currentContactId.value = data?.id + currentContactIdQuery.value = data?.id + } + + const updateContactField = ({ + id, + field, + value + }: { + id: T['id'] + field: keyof T + value: T[keyof T] + }) => { + const contact = unref(contacts).find((contact) => id === contact.id) as T + if (contact) { + contact[field] = value + } + } + + const reset = () => { + contacts.value = [] + currentContactId.value = null + currentContactIdQuery.value = null + } + + return { + contacts, + currentContact, + updateContactField, + setContacts, + upsertContact, + removeContacts, + setCurrentContact, + reset + } +}) + +export type ContactsStore = ReturnType diff --git a/packages/web-app-contacts/src/composables/useLoadAddressbooks.ts b/packages/web-app-contacts/src/composables/useLoadAddressbooks.ts new file mode 100644 index 0000000000..6fede17c18 --- /dev/null +++ b/packages/web-app-contacts/src/composables/useLoadAddressbooks.ts @@ -0,0 +1,42 @@ +import { computed } from 'vue' +import { useTask } from 'vue-concurrency' +import { useClientService, useConfigStore } from '@opencloud-eu/web-pkg' +import { useAddressBooksStore } from './piniaStores/addressbooks' +import { AddressBooksResponseSchema } from '../types' +import { urlJoin } from '@opencloud-eu/web-client' + +let loadAddressBooksTask: ReturnType | null = null +const isLoading = computed(() => loadAddressBooksTask?.isRunning ?? false) + +export const useLoadAddressBooks = () => { + const configStore = useConfigStore() + const clientService = useClientService() + const { setAddressBooks } = useAddressBooksStore() + + if (!loadAddressBooksTask) { + loadAddressBooksTask = useTask(function* (signal, accountId: string) { + try { + const { data } = yield clientService.httpAuthenticated.get( + urlJoin(configStore.groupwareUrl, `accounts/${accountId}/addressbooks`) + ) + const { addressbooks } = AddressBooksResponseSchema.parse(data) + setAddressBooks(addressbooks) + console.info('Loaded addressBooks:', addressbooks) + return addressbooks + } catch (e) { + console.error('Failed to load addressBooks:', e) + throw e + } + }).restartable() + } + + const loadAddressBooks = (accountId: string) => { + return loadAddressBooksTask!.perform(accountId) + } + + return { + loadAddressBooks, + loadAddressBooksTask, + isLoading + } +} diff --git a/packages/web-app-contacts/src/composables/useLoadContacts.ts b/packages/web-app-contacts/src/composables/useLoadContacts.ts new file mode 100644 index 0000000000..2fbf7f611c --- /dev/null +++ b/packages/web-app-contacts/src/composables/useLoadContacts.ts @@ -0,0 +1,46 @@ +import { computed } from 'vue' +import { useTask } from 'vue-concurrency' +import { useClientService, useConfigStore } from '@opencloud-eu/web-pkg' +import { useContactsStore } from './piniaStores/contacts' +import { ContactSchema } from '../types' +import { urlJoin } from '@opencloud-eu/web-client' +import { z } from 'zod' + +let loadContactsTask: ReturnType | null = null +const isLoading = computed(() => loadContactsTask?.isRunning ?? false) + +export const useLoadContacts = () => { + const configStore = useConfigStore() + const clientService = useClientService() + const { setContacts } = useContactsStore() + + if (!loadContactsTask) { + loadContactsTask = useTask(function* (signal, accountId: string, addressBookId: string) { + try { + const { data } = yield clientService.httpAuthenticated.get( + urlJoin( + configStore.groupwareUrl, + `accounts/${accountId}/addressbooks/${addressBookId}/contacts` + ) + ) + const contacts = z.array(ContactSchema).parse(data) + setContacts(contacts) + console.info('Loaded contacts:', contacts) + return contacts + } catch (e) { + console.error('Failed to load contacts:', e) + throw e + } + }).restartable() + } + + const loadContacts = (accountId: string, addressBookId: string) => { + return loadContactsTask!.perform(accountId, addressBookId) + } + + return { + loadContacts, + loadContactsTask, + isLoading + } +} diff --git a/packages/web-app-contacts/src/extensions.ts b/packages/web-app-contacts/src/extensions.ts new file mode 100644 index 0000000000..508f97d910 --- /dev/null +++ b/packages/web-app-contacts/src/extensions.ts @@ -0,0 +1,71 @@ +import { urlJoin } from '@opencloud-eu/web-client' +import { + AppMenuItemExtension, + FloatingActionButtonExtension, + CustomComponentExtension, + AccountsSwitch, + ApplicationInformation, + useCapabilityStore, + useUserStore, + Extension +} from '@opencloud-eu/web-pkg' +import { $gettext } from '@opencloud-eu/web-pkg/src/router/utils' +import { computed, unref } from 'vue' +import { storeToRefs } from 'pinia' +import AddressBooksList from './components/AddressBooksList.vue' + +export const extensions = (appInfo: ApplicationInformation) => { + const capabilityStore = useCapabilityStore() + const userStore = useUserStore() + const { user } = storeToRefs(userStore) + + const menuItemExtension: AppMenuItemExtension = { + id: `app.${appInfo.id}.menuItem`, + type: 'appMenuItem', + label: () => appInfo.name, + color: appInfo.color, + icon: appInfo.icon, + priority: 30, + path: urlJoin(appInfo.id) + } + + const floatingActionButton: FloatingActionButtonExtension = { + id: `com.github.opencloud-eu.web.${appInfo.id}.floating-action-button`, + extensionPointIds: [`app.${appInfo.id}.floating-action-button`], + type: 'floatingActionButton', + icon: 'add', + isDisabled: () => true, + label: () => $gettext('New'), + mode: () => 'handler', + handler: () => { + console.log('Create new contact') + } + } + + const mainNavExtension: CustomComponentExtension = { + id: `app.${appInfo.id}.sidebar-nav.main-content`, + extensionPointIds: [`app.${appInfo.id}.sidebar-nav.main`], + type: 'customComponent', + content: AddressBooksList + } + + const bottomNavExtension: CustomComponentExtension = { + id: `app.${appInfo.id}.sidebar-nav.bottom-content`, + extensionPointIds: [`app.${appInfo.id}.sidebar-nav.bottom`], + type: 'customComponent', + content: AccountsSwitch + } + + return computed(() => { + const result: Extension[] = [] + + if (unref(user) && capabilityStore.capabilities.groupware?.enabled) { + result.push(menuItemExtension) + result.push(floatingActionButton) + result.push(mainNavExtension) + result.push(bottomNavExtension) + } + + return result + }) +} diff --git a/packages/web-app-contacts/src/index.ts b/packages/web-app-contacts/src/index.ts new file mode 100644 index 0000000000..d7b54c5b87 --- /dev/null +++ b/packages/web-app-contacts/src/index.ts @@ -0,0 +1,51 @@ +import translations from '../l10n/translations.json' +import { useGettext } from 'vue3-gettext' +import Contacts from './views/Contacts.vue' +import LayoutContainer from './LayoutContainer.vue' + +import { defineWebApplication } from '@opencloud-eu/web-pkg' +import { APPID } from './appid' +import { RouteRecordRaw } from 'vue-router' +import { extensions } from './extensions' + +export default defineWebApplication({ + setup() { + const { $gettext } = useGettext() + + const appInfo = { + name: $gettext('Contacts'), + id: APPID, + icon: 'contacts-book-2', + color: '#1d8cf8' + } + + const routes: RouteRecordRaw[] = [ + { + path: '', + name: 'contacts-root', + component: LayoutContainer, + meta: { + authContext: 'user' + }, + children: [ + { + path: '', + name: 'contacts-view', + component: Contacts, + meta: { + authContext: 'user', + title: $gettext('Contacts') + } + } + ] + } + ] + + return { + appInfo, + routes, + translations, + extensions: extensions(appInfo) + } + } +}) diff --git a/packages/web-app-contacts/src/types.ts b/packages/web-app-contacts/src/types.ts new file mode 100644 index 0000000000..d56769a3cd --- /dev/null +++ b/packages/web-app-contacts/src/types.ts @@ -0,0 +1,173 @@ +import { z } from 'zod' + +export const AddressBookRightsSchema = z.object({ + mayAdmin: z.boolean(), + mayDelete: z.boolean(), + mayRead: z.boolean(), + mayWrite: z.boolean() +}) + +export const AddressBookSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + isDefault: z.boolean().optional(), + isSubscribed: z.boolean().optional(), + myRights: AddressBookRightsSchema +}) + +export const AddressBooksResponseSchema = z.object({ + addressbooks: z.array(AddressBookSchema) +}) + +export type AddressBook = z.infer +export type AddressBookRights = z.infer +export type AddressBooksResponse = z.infer + +// Contact schemas +export const AddressBookIdsSchema = z.record(z.string(), z.boolean()) +export const KeywordsSchema = z.record(z.string(), z.boolean()) + +export const AddressComponentSchema = z.object({ + kind: z.string(), + value: z.string() +}) + +export const ContextsSchema = z.record(z.string(), z.boolean()) + +export const AddressSchema = z.object({ + components: z.array(AddressComponentSchema).optional(), + contexts: ContextsSchema.optional(), + countryCode: z.string().optional() +}) + +export const PartialDateSchema = z.object({ + '@type': z.literal('PartialDate'), + day: z.number().optional(), + month: z.number().optional(), + year: z.number().optional() +}) + +export const AnniversarySchema = z.object({ + date: PartialDateSchema.optional(), + kind: z.string().optional() +}) + +export const EmailSchema = z.object({ + address: z.string(), + contexts: ContextsSchema.optional() +}) + +export const MediaSchema = z.object({ + blobId: z.string().optional(), + uri: z.string().optional(), + contexts: ContextsSchema.optional(), + kind: z.string().optional(), + mediaType: z.string().optional() +}) + +export const NameComponentSchema = z.object({ + kind: z.string(), + value: z.string() +}) + +export const NameSchema = z.object({ + '@type': z.literal('Name'), + components: z.array(NameComponentSchema).optional(), + defaultSeparator: z.string().optional(), + isOrdered: z.boolean().optional() +}) + +export const NicknameSchema = z.object({ + name: z.string(), + contexts: ContextsSchema.optional() +}) + +export const NoteAuthorSchema = z.object({ + name: z.string().optional() +}) + +export const NoteSchema = z.object({ + author: NoteAuthorSchema.optional(), + created: z.string().optional(), + note: z.string() +}) + +export const OnlineServiceSchema = z.object({ + service: z.string(), + user: z.string(), + contexts: ContextsSchema.optional() +}) + +export const OrganizationUnitSchema = z.object({ + name: z.string() +}) + +export const OrganizationSchema = z.object({ + name: z.string(), + units: z.array(OrganizationUnitSchema).optional() +}) + +export const PhoneSchema = z.object({ + number: z.string(), + contexts: ContextsSchema.optional() +}) + +export const PreferredLanguageSchema = z.object({ + language: z.string(), + pref: z.number().optional(), + contexts: ContextsSchema.optional() +}) + +export const RelationSchema = z.record(z.string(), z.boolean()) + +export const RelatedToSchema = z.object({ + relation: RelationSchema.optional() +}) + +export const SpeakToAsSchema = z.object({ + grammaticalGender: z.string().optional(), + pronouns: z.string().optional() +}) + +export const TitleSchema = z.object({ + name: z.string(), + kind: z.string().optional(), + organizationId: z.string().optional() +}) + +export const ContactSchema = z.object({ + '@type': z.literal('Card'), + id: z.string(), + uid: z.string().optional(), + version: z.string().optional(), + created: z.string().optional(), + updated: z.string().optional(), + prodId: z.string().optional(), + kind: z.string().optional(), + language: z.string().optional(), + addressBookIds: AddressBookIdsSchema.optional(), + keywords: KeywordsSchema.optional(), + name: NameSchema.optional(), + addresses: z.record(z.string(), AddressSchema).optional(), + anniversaries: z.record(z.string(), AnniversarySchema).optional(), + emails: z.record(z.string(), EmailSchema).optional(), + media: z.record(z.string(), MediaSchema).optional(), + nicknames: z.record(z.string(), NicknameSchema).optional(), + notes: z.record(z.string(), NoteSchema).optional(), + onlineServices: z.record(z.string(), OnlineServiceSchema).optional(), + organizations: z.record(z.string(), OrganizationSchema).optional(), + phones: z.record(z.string(), PhoneSchema).optional(), + preferredLanguages: z.record(z.string(), PreferredLanguageSchema).optional(), + relatedTo: z.record(z.string(), RelatedToSchema).optional(), + speakToAs: SpeakToAsSchema.optional(), + titles: z.record(z.string(), TitleSchema).optional() +}) + +export type Contact = z.infer +export type ContactName = z.infer +export type ContactEmail = z.infer +export type ContactPhone = z.infer +export type ContactAddress = z.infer +export type ContactOrganization = z.infer +export type ContactMedia = z.infer diff --git a/packages/web-app-contacts/src/views/Contacts.vue b/packages/web-app-contacts/src/views/Contacts.vue new file mode 100644 index 0000000000..25ee6daca2 --- /dev/null +++ b/packages/web-app-contacts/src/views/Contacts.vue @@ -0,0 +1,69 @@ + + + diff --git a/packages/web-app-mail/src/components/MailboxTree.vue b/packages/web-app-mail/src/components/MailboxTree.vue index 162595bafb..1c104dd96c 100644 --- a/packages/web-app-mail/src/components/MailboxTree.vue +++ b/packages/web-app-mail/src/components/MailboxTree.vue @@ -1,64 +1,53 @@ diff --git a/packages/web-app-contacts/src/extensions.ts b/packages/web-app-contacts/src/extensions.ts index 508f97d910..6b90bfa4b4 100644 --- a/packages/web-app-contacts/src/extensions.ts +++ b/packages/web-app-contacts/src/extensions.ts @@ -34,7 +34,6 @@ export const extensions = (appInfo: ApplicationInformation) => { extensionPointIds: [`app.${appInfo.id}.floating-action-button`], type: 'floatingActionButton', icon: 'add', - isDisabled: () => true, label: () => $gettext('New'), mode: () => 'handler', handler: () => { diff --git a/packages/web-app-contacts/src/types.ts b/packages/web-app-contacts/src/types.ts index d56769a3cd..243a54fece 100644 --- a/packages/web-app-contacts/src/types.ts +++ b/packages/web-app-contacts/src/types.ts @@ -42,7 +42,7 @@ export const AddressSchema = z.object({ }) export const PartialDateSchema = z.object({ - '@type': z.literal('PartialDate'), + '@type': z.string().optional(), day: z.number().optional(), month: z.number().optional(), year: z.number().optional() @@ -72,7 +72,7 @@ export const NameComponentSchema = z.object({ }) export const NameSchema = z.object({ - '@type': z.literal('Name'), + '@type': z.string().optional(), components: z.array(NameComponentSchema).optional(), defaultSeparator: z.string().optional(), isOrdered: z.boolean().optional() @@ -94,8 +94,8 @@ export const NoteSchema = z.object({ }) export const OnlineServiceSchema = z.object({ - service: z.string(), - user: z.string(), + service: z.string().optional(), + user: z.string().optional(), contexts: ContextsSchema.optional() }) @@ -137,7 +137,7 @@ export const TitleSchema = z.object({ }) export const ContactSchema = z.object({ - '@type': z.literal('Card'), + '@type': z.string().optional(), id: z.string(), uid: z.string().optional(), version: z.string().optional(), diff --git a/packages/web-app-contacts/src/views/Contacts.vue b/packages/web-app-contacts/src/views/Contacts.vue index 25ee6daca2..5de496d2a7 100644 --- a/packages/web-app-contacts/src/views/Contacts.vue +++ b/packages/web-app-contacts/src/views/Contacts.vue @@ -1,28 +1,27 @@ From dcd0918f4d322a5767a0156ae46f52f1f28123ac Mon Sep 17 00:00:00 2001 From: Alexander Ackermann Date: Wed, 18 Mar 2026 21:16:31 +0100 Subject: [PATCH 3/7] add sorting --- .../src/components/ContactsListItem.vue | 13 +++---- .../composables/piniaStores/addressbooks.ts | 2 +- .../src/composables/piniaStores/contacts.ts | 2 +- packages/web-app-contacts/src/extensions.ts | 3 +- .../src/helpers/contactName.ts | 12 +++++++ .../web-app-contacts/src/views/Contacts.vue | 2 +- .../src/views/ContactsList.vue | 34 ++++++++++++++++--- 7 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 packages/web-app-contacts/src/helpers/contactName.ts diff --git a/packages/web-app-contacts/src/components/ContactsListItem.vue b/packages/web-app-contacts/src/components/ContactsListItem.vue index 66931e83e4..bb835bb2d9 100644 --- a/packages/web-app-contacts/src/components/ContactsListItem.vue +++ b/packages/web-app-contacts/src/components/ContactsListItem.vue @@ -11,25 +11,22 @@ diff --git a/packages/web-app-contacts/src/composables/piniaStores/addressbooks.ts b/packages/web-app-contacts/src/composables/piniaStores/addressbooks.ts index acf765230e..7bcc8c3d5f 100644 --- a/packages/web-app-contacts/src/composables/piniaStores/addressbooks.ts +++ b/packages/web-app-contacts/src/composables/piniaStores/addressbooks.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia' import { computed, ref, unref } from 'vue' import { AddressBook } from '../../types' -import { useRouteQuery } from '@opencloud-eu/web-pkg/src' +import { useRouteQuery } from '@opencloud-eu/web-pkg' export const useAddressBooksStore = defineStore('addressBooks', () => { const currentAddressBookIdQuery = useRouteQuery('addressBookId') diff --git a/packages/web-app-contacts/src/composables/piniaStores/contacts.ts b/packages/web-app-contacts/src/composables/piniaStores/contacts.ts index 872513d70d..f49d962a99 100644 --- a/packages/web-app-contacts/src/composables/piniaStores/contacts.ts +++ b/packages/web-app-contacts/src/composables/piniaStores/contacts.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia' import { computed, ref, unref } from 'vue' import { Contact } from '../../types' -import { useRouteQuery } from '@opencloud-eu/web-pkg/src' +import { useRouteQuery } from '@opencloud-eu/web-pkg' export const useContactsStore = defineStore('contacts', () => { const currentContactIdQuery = useRouteQuery('contactId') diff --git a/packages/web-app-contacts/src/extensions.ts b/packages/web-app-contacts/src/extensions.ts index 6b90bfa4b4..ed4a948de7 100644 --- a/packages/web-app-contacts/src/extensions.ts +++ b/packages/web-app-contacts/src/extensions.ts @@ -9,15 +9,16 @@ import { useUserStore, Extension } from '@opencloud-eu/web-pkg' -import { $gettext } from '@opencloud-eu/web-pkg/src/router/utils' import { computed, unref } from 'vue' import { storeToRefs } from 'pinia' import AddressBooksList from './components/AddressBooksList.vue' +import { useGettext } from 'vue3-gettext' export const extensions = (appInfo: ApplicationInformation) => { const capabilityStore = useCapabilityStore() const userStore = useUserStore() const { user } = storeToRefs(userStore) + const { $gettext } = useGettext() const menuItemExtension: AppMenuItemExtension = { id: `app.${appInfo.id}.menuItem`, diff --git a/packages/web-app-contacts/src/helpers/contactName.ts b/packages/web-app-contacts/src/helpers/contactName.ts new file mode 100644 index 0000000000..995b9366b9 --- /dev/null +++ b/packages/web-app-contacts/src/helpers/contactName.ts @@ -0,0 +1,12 @@ +import type { Contact } from '../types' + +export const getContactNameParts = (contact: Contact) => { + const components = contact.name?.components || [] + const givenName = components.find((component) => component.kind === 'given')?.value?.trim() || '' + const surname = components.find((component) => component.kind === 'surname')?.value?.trim() || '' + + return { + givenName, + surname + } +} diff --git a/packages/web-app-contacts/src/views/Contacts.vue b/packages/web-app-contacts/src/views/Contacts.vue index 5de496d2a7..4640d9a6e8 100644 --- a/packages/web-app-contacts/src/views/Contacts.vue +++ b/packages/web-app-contacts/src/views/Contacts.vue @@ -20,7 +20,7 @@ import { useAddressBooksStore } from '../composables/piniaStores/addressbooks' import { useLoadAddressBooks } from '../composables/useLoadAddressbooks' import { AddressBook } from '../types' import { useLoadContacts } from '../composables/useLoadContacts' -import { AppLoadingSpinner } from '@opencloud-eu/web-pkg/src' +import { AppLoadingSpinner } from '@opencloud-eu/web-pkg' import ContactsList from './ContactsList.vue' const { httpAuthenticated } = useClientService() diff --git a/packages/web-app-contacts/src/views/ContactsList.vue b/packages/web-app-contacts/src/views/ContactsList.vue index deebe4266d..2462ceabd7 100644 --- a/packages/web-app-contacts/src/views/ContactsList.vue +++ b/packages/web-app-contacts/src/views/ContactsList.vue @@ -3,7 +3,7 @@