Skip to content
Merged
10 changes: 9 additions & 1 deletion packages/design-system/src/components/OcModal/OcModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
@keydown.esc.stop="cancelModalAction"
>
<div
class="oc-modal-title bg-role-surface-container flex items-center flex-row flex-wrap py-3 px-4 rounded-t-sm"
class="oc-modal-title bg-role-surface-container flex items-center flex-row flex-wrap justify-between py-3 px-4 rounded-t-sm"
>
<h2 id="oc-modal-title" class="truncate m-0 text-base" v-text="title" />
<div v-if="$slots['headerActions']" class="flex items-center gap-1">
<slot name="headerActions" />
</div>
</div>
<div class="oc-modal-body px-4 pt-4">
<div
Expand Down Expand Up @@ -211,6 +214,11 @@ export interface Slots {
* @docs Custom content of the modal.
*/
content?: () => unknown

/**
* @docs Custom actions in the modal header.
*/
headerActions?: () => unknown
}

const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ exports[`OcModal > displays input 1`] = `
"<div class="oc-modal-background fixed left-0 top-0 z-[var(--z-index-modal)] bg-black/40 flex items-center justify-center flex-row flex-wrap size-full">
<focus-trap-stub tabbableoptions="[object Object]" active="true" escapedeactivates="true" returnfocusondeactivate="true" allowoutsideclick="true" clickoutsidedeactivates="false" initialfocus="[Function]" delayinitialfocus="true" preventscroll="false" setreturnfocus="false">
<div class="oc-modal bg-role-surface border z-[calc(var(--z-index-modal)+1)] border-role-outline rounded-lg focus:outline-0 w-full max-w-xl max-h-[90vh] overflow-auto shadow-2xl" tabindex="0" role="dialog" aria-modal="true" aria-labelledby="oc-modal-title">
<div class="oc-modal-title bg-role-surface-container flex items-center flex-row flex-wrap py-3 px-4 rounded-t-sm">
<div class="oc-modal-title bg-role-surface-container flex items-center flex-row flex-wrap justify-between py-3 px-4 rounded-t-sm">
<h2 id="oc-modal-title" class="truncate m-0 text-base">Create new folder</h2>
<!--v-if-->
</div>
<div class="oc-modal-body px-4 pt-4">
<!--v-if-->
Expand All @@ -27,8 +28,9 @@ exports[`OcModal > displays loading state 1`] = `
"<div class="oc-modal-background fixed left-0 top-0 z-[var(--z-index-modal)] bg-black/40 flex items-center justify-center flex-row flex-wrap size-full">
<focus-trap-stub tabbableoptions="[object Object]" active="true" escapedeactivates="true" returnfocusondeactivate="true" allowoutsideclick="true" clickoutsidedeactivates="false" initialfocus="[Function]" delayinitialfocus="true" preventscroll="false" setreturnfocus="false">
<div class="oc-modal bg-role-surface border z-[calc(var(--z-index-modal)+1)] border-role-outline rounded-lg focus:outline-0 w-full max-w-xl max-h-[90vh] overflow-auto shadow-2xl" tabindex="0" role="dialog" aria-modal="true" aria-labelledby="oc-modal-title">
<div class="oc-modal-title bg-role-surface-container flex items-center flex-row flex-wrap py-3 px-4 rounded-t-sm">
<div class="oc-modal-title bg-role-surface-container flex items-center flex-row flex-wrap justify-between py-3 px-4 rounded-t-sm">
<h2 id="oc-modal-title" class="truncate m-0 text-base">Example title</h2>
<!--v-if-->
</div>
<div class="oc-modal-body px-4 pt-4">
<p class="oc-modal-body-message mt-0 mb-0">Example message</p>
Expand All @@ -50,8 +52,9 @@ exports[`OcModal > matches snapshot 1`] = `
"<div class="oc-modal-background fixed left-0 top-0 z-[var(--z-index-modal)] bg-black/40 flex items-center justify-center flex-row flex-wrap size-full">
<focus-trap-stub tabbableoptions="[object Object]" active="true" escapedeactivates="true" returnfocusondeactivate="true" allowoutsideclick="true" clickoutsidedeactivates="false" initialfocus="[Function]" delayinitialfocus="true" preventscroll="false" setreturnfocus="false">
<div class="oc-modal bg-role-surface border z-[calc(var(--z-index-modal)+1)] border-role-outline rounded-lg focus:outline-0 w-full max-w-xl max-h-[90vh] overflow-auto shadow-2xl" tabindex="0" role="dialog" aria-modal="true" aria-labelledby="oc-modal-title">
<div class="oc-modal-title bg-role-surface-container flex items-center flex-row flex-wrap py-3 px-4 rounded-t-sm">
<div class="oc-modal-title bg-role-surface-container flex items-center flex-row flex-wrap justify-between py-3 px-4 rounded-t-sm">
<h2 id="oc-modal-title" class="truncate m-0 text-base">Example title</h2>
<!--v-if-->
</div>
<div class="oc-modal-body px-4 pt-4">
<p class="oc-modal-body-message mt-0 mb-0">Example message</p>
Expand All @@ -73,8 +76,9 @@ exports[`OcModal > overrides props message with slot 1`] = `
"<div class="oc-modal-background fixed left-0 top-0 z-[var(--z-index-modal)] bg-black/40 flex items-center justify-center flex-row flex-wrap size-full">
<focus-trap-stub tabbableoptions="[object Object]" active="true" escapedeactivates="true" returnfocusondeactivate="true" allowoutsideclick="true" clickoutsidedeactivates="false" initialfocus="[Function]" delayinitialfocus="true" preventscroll="false" setreturnfocus="false">
<div class="oc-modal bg-role-surface border z-[calc(var(--z-index-modal)+1)] border-role-outline rounded-lg focus:outline-0 w-full max-w-xl max-h-[90vh] overflow-auto shadow-2xl" tabindex="0" role="dialog" aria-modal="true" aria-labelledby="oc-modal-title">
<div class="oc-modal-title bg-role-surface-container flex items-center flex-row flex-wrap py-3 px-4 rounded-t-sm">
<div class="oc-modal-title bg-role-surface-container flex items-center flex-row flex-wrap justify-between py-3 px-4 rounded-t-sm">
<h2 id="oc-modal-title" class="truncate m-0 text-base">Example title</h2>
<!--v-if-->
</div>
<div class="oc-modal-body px-4 pt-4">
<div class="oc-modal-body-message mt-2 mb-4">
Expand Down
140 changes: 140 additions & 0 deletions packages/web-app-mail/src/components/MailComposeForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<template>
<div class="px-4">
<div class="py-2 mb-2 border-b border-role-outline-variant">
<oc-select
:model-value="modelValue.from"
:label="`${$gettext('From')}:`"
:inline-label="true"
:has-border="false"
:options="fromOptions"
option-label="label"
option-value="value"
class="w-full"
@update:model-value="(value: FromOption) => updateField('from', value)"
/>
</div>

<oc-text-input
:model-value="modelValue.to"
type="email"
class="mail-new-message-to-input mb-2 pb-2 border-b border-role-outline-variant"
:label="`${$gettext('To')}:`"
:inline-label="true"
:has-border="false"
@update:model-value="(value: string) => updateField('to', value)"
/>

<oc-text-input
:model-value="modelValue.cc"
type="email"
class="mail-new-message-cc-input mb-2 pb-2 border-b border-role-outline-variant"
:label="`${$gettext('CC')}:`"
:inline-label="true"
:has-border="false"
@update:model-value="(value: string) => updateField('cc', value)"
/>

<oc-text-input
:model-value="modelValue.bcc"
type="email"
class="mail-new-message-bcc-input mb-2 pb-2 border-b border-role-outline-variant"
:label="`${$gettext('BCC')}:`"
:inline-label="true"
:has-border="false"
@update:model-value="(value: string) => updateField('bcc', value)"
/>

<oc-text-input
:model-value="modelValue.subject"
class="mail-new-message-to-input pb-2 border-b border-role-outline-variant"
:label="`${$gettext('Subject')}:`"
:inline-label="true"
:has-border="false"
@update:model-value="(value: string) => updateField('subject', value)"
/>

<div class="py-4">
<oc-textarea
:model-value="modelValue.body"
:label="$gettext('Write email')"
@update:model-value="(value: string) => updateField('body', value)"
/>
</div>
</div>
</template>

<script setup lang="ts">
import { computed, unref, watch } from 'vue'
import { useGettext } from 'vue3-gettext'
import { useRouteQuery } from '@opencloud-eu/web-pkg'
import { storeToRefs } from 'pinia'
import { useAccountsStore } from '../composables/piniaStores/accounts'

type FromOption = {
value: string
label: string
email: string
accountId: string
identityId: string
}

export type ComposeFormState = {
from?: FromOption
to: string
cc: string
bcc: string
subject: string
body: string
}

const props = defineProps<{
modelValue: ComposeFormState
}>()

const emit = defineEmits<{
(e: 'update:modelValue', value: ComposeFormState): void
}>()

const { $gettext } = useGettext()

const accountsStore = useAccountsStore()
const { accounts } = storeToRefs(accountsStore)

const selectedAccountIdQuery = useRouteQuery('accountId')

const fromOptions = computed<FromOption[]>(() => {
return (
unref(accounts)?.flatMap((account) =>
account.identities?.map((identity) => ({
label: identity.name ? `${identity.name} <${identity.email}>` : identity.email,
value: identity.id,
email: identity.email,
accountId: account.accountId,
identityId: identity.id
}))
) ?? []
)
})

const updateField = <K extends keyof ComposeFormState>(key: K, value: ComposeFormState[K]) => {
emit('update:modelValue', { ...props.modelValue, [key]: value })
}

watch(
fromOptions,
(options) => {
if (!options.length) {
return
}
const selectedAccountId = unref(selectedAccountIdQuery)
const defaultFrom = selectedAccountId
? (options.find((o) => o.accountId === selectedAccountId) ?? options[0])
: options[0]

if (!props.modelValue.from && defaultFrom) {
updateField('from', defaultFrom)
}
},
{ immediate: true }
)
</script>
28 changes: 21 additions & 7 deletions packages/web-app-mail/src/components/MailList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
class="md:hidden"
mode="action"
:aria-label="$gettext('Write new Email')"
:to="{ name: 'mail-create', query: { ...route.query, draftId: 'new' } }"
@click="showCompose = true"
/>
<no-content-message v-if="!currentMailbox" icon="folder" icon-fill-type="line">
<template #message>
<span v-text="$gettext('No mailbox selected')" />
</template>
</no-content-message>

<template v-else>
<div class="flex w-full items-center justify-between md:justify-normal">
<oc-button
Expand All @@ -23,9 +24,10 @@
>
<oc-icon name="arrow-left" fill-type="line" />
</oc-button>
<h2 class="text-lg ml-4" v-text="currentMailbox.name"></h2>
<div class="paceholder" />
<h2 class="text-lg ml-4" v-text="currentMailbox.name" />
<div class="placeholder" />
</div>

<no-content-message
v-if="!mails || !mails.length"
class="mail-list-empty"
Expand All @@ -36,6 +38,7 @@
<span v-text="$gettext('No mails in this mailbox')" />
</template>
</no-content-message>

<oc-list v-else class="mail-list">
<li
v-for="mail in mails"
Expand All @@ -55,26 +58,27 @@
</oc-button>
</li>
</oc-list>
<MailWidget v-model="showCompose" />
</template>
</template>
</template>

<script setup lang="ts">
import { AppLoadingSpinner, NoContentMessage } from '@opencloud-eu/web-pkg'
import MailListItem from './MailListItem.vue'
import { Mail } from '../types'
import { useRoute } from 'vue-router'
import MailWidget from './MailWidget.vue'
import type { Mail } from '../types'
import { useLoadMails } from '../composables/useLoadMails'
import { useMailsStore } from '../composables/piniaStores/mails'
import { useMailboxesStore } from '../composables/piniaStores/mailboxes'
import { storeToRefs } from 'pinia'
import { useLoadMail } from '../composables/useLoadMail'
import { unref } from 'vue'
import { ref, unref } from 'vue'
import { useAccountsStore } from '../composables/piniaStores/accounts'

const route = useRoute()
const accountsStore = useAccountsStore()
const { currentAccount } = storeToRefs(accountsStore)

const mailsStore = useMailsStore()
const mailboxesStore = useMailboxesStore()
const { currentMail, mails } = storeToRefs(mailsStore)
Expand All @@ -84,6 +88,16 @@ const { setCurrentMailbox } = mailboxesStore
const { loadMail } = useLoadMail()
const { isLoading } = useLoadMails()

const showCompose = ref(false)

const openCompose = () => {
showCompose.value = true
}

defineExpose({
openCompose
})

const onNavigateBack = () => {
setCurrentMailbox(null)
setCurrentMail(null)
Expand Down
Loading