diff options
author | Christoph Wurst <christoph@winzerhof-wurst.at> | 2019-03-06 17:36:12 +0300 |
---|---|---|
committer | Christoph Wurst <christoph@winzerhof-wurst.at> | 2019-03-06 17:36:12 +0300 |
commit | bcad37da3378ddfba74eceaf39465543c5610249 (patch) | |
tree | 5a28b393c4251264432b577740114be2608ce494 /src | |
parent | b18aaf9fce3c5a6f4afed82f41952cfd44b23a7a (diff) | |
parent | 5531b41a17713965b1ecc9be07337f3615355e18 (diff) |
Merge branch 'master' of github.com:/nextcloud/mail into fix/vue-design-regressions
Diffstat (limited to 'src')
-rw-r--r-- | src/components/AccountForm.vue | 762 | ||||
-rw-r--r-- | src/service/AccountService.js | 16 | ||||
-rw-r--r-- | src/store/actions.js | 9 | ||||
-rw-r--r-- | src/store/actions.js.orig | 865 | ||||
-rw-r--r-- | src/store/mutations.js | 7 | ||||
-rw-r--r-- | src/views/AccountSettings.vue | 50 |
6 files changed, 1336 insertions, 373 deletions
diff --git a/src/components/AccountForm.vue b/src/components/AccountForm.vue index 9aa35f657..f556c6b4d 100644 --- a/src/components/AccountForm.vue +++ b/src/components/AccountForm.vue @@ -1,398 +1,432 @@ <template> - <div id="account-form"> - <tabs :options="{ useUrlFragment: false, defaultTabHash: 'auto' }" - @changed="onModeChanged" - cache-lifetime="0"> - <tab :name="t('mail', 'Auto')" - id="auto" - key="auto"> - <label for="auto-name"> - {{ t('mail', 'Name') }} - </label> - <input type="text" - id="auto-name" - :placeholder="t('mail', 'Name')" - v-model="autoConfig.accountName" - :disabled="loading" - autofocus/> - <label for="auto-address"> - {{ t('mail', 'Mail Address') }} - </label> - <input type="email" - id="auto-address" - :placeholder="t('mail', 'Mail Address')" - v-model="autoConfig.emailAddress" - :disabled="loading" - required/> - <label for="auto-password"> - {{ t('mail', 'Password') }} - </label> - <input type="password" - id="auto-password" - :placeholder="t('mail', 'Password')" - v-model="autoConfig.password" - :disabled="loading" - required/> - </tab> - <tab :name="t('mail', 'Manual')" - id="manual" - key="manual"> - <label for="man-name"> - {{ t('mail', 'Name') }} - </label> - <input type="text" - id="man-name" - :placeholder="t('mail', 'Name')" - v-model="manualConfig.accountName" - :disabled="loading" - autofocus/> - <label for="man-address"> - {{ t('mail', 'Mail Address') }} - </label> - <input type="email" - id="man-address" - :placeholder="t('mail', 'Mail Address')" - v-model="manualConfig.emailAddress" - :disabled="loading" - required/> + <div id="account-form"> + <tabs + :options="{ useUrlFragment: false, defaultTabHash: settingsPage ? 'manual' : 'auto' }" + @changed="onModeChanged" + cache-lifetime="0" + > + <tab :name="t('mail', 'Auto')" id="auto" key="auto"> + <label for="auto-name">{{ t('mail', 'Name') }}</label> + <input + type="text" + id="auto-name" + :placeholder="t('mail', 'Name')" + v-model="autoConfig.accountName" + :disabled="loading" + autofocus + > + <label for="auto-address">{{ t('mail', 'Mail Address') }}</label> + <input + type="email" + id="auto-address" + :placeholder="t('mail', 'Mail Address')" + v-model="autoConfig.emailAddress" + :disabled="loading" + required + > + <label for="auto-password">{{ t('mail', 'Password') }}</label> + <input + type="password" + id="auto-password" + :placeholder="t('mail', 'Password')" + v-model="autoConfig.password" + :disabled="loading" + required + > + </tab> + <tab :name="t('mail', 'Manual')" id="manual" key="manual"> + <label for="man-name">{{ t('mail', 'Name') }}</label> + <input + type="text" + id="man-name" + :placeholder="t('mail', 'Name')" + v-model="manualConfig.accountName" + :disabled="loading" + autofocus + > + <label for="man-address">{{ t('mail', 'Mail Address') }}</label> + <input + type="email" + id="man-address" + :placeholder="t('mail', 'Mail Address')" + v-model="manualConfig.emailAddress" + :disabled="loading" + required + > - <h3>{{ t('mail', 'IMAP Settings') }}</h3> - <label for="man-imap-host"> - {{ t('mail', 'IMAP Host') }} - </label> - <input type="text" - id="man-imap-host" - :placeholder="t('mail', 'IMAP Host')" - v-model="manualConfig.imapHost" - :disabled="loading" - required/> - <h4>{{ t('mail', 'IMAP Security') }}</h4> - <div class="flex-row"> - <input type="radio" - id="man-imap-sec-none" - name="man-imap-sec" - v-model="manualConfig.imapSslMode" - :disabled="loading" - @change="onImapSslModeChange" - value="none"> - <label class="button" - for="man-imap-sec-none" - :class="{primary: manualConfig.imapSslMode === 'none' }"> - {{ t('mail', 'None') }} - </label> - <input type="radio" - id="man-imap-sec-ssl" - name="man-imap-sec" - v-model="manualConfig.imapSslMode" - :disabled="loading" - @change="onImapSslModeChange" - value="ssl"> - <label class="button" - for="man-imap-sec-ssl" - :class="{primary: manualConfig.imapSslMode === 'ssl' }"> - {{ t('mail', 'SSL/TLS') }} - </label> - <input type="radio" - id="man-imap-sec-tls" - name="man-imap-sec" - v-model="manualConfig.imapSslMode" - :disabled="loading" - @change="onImapSslModeChange" - value="tls"> - <label class="button" - for="man-imap-sec-tls" - :class="{primary: manualConfig.imapSslMode === 'tls' }"> - {{ t('mail', 'STARTTLS') }} - </label> - </div> - <label for="man-imap-port"> - {{ t('mail', 'IMAP Port') }} - </label> - <input type="number" - id="man-imap-port" - :placeholder="t('mail', 'IMAP Port')" - v-model="manualConfig.imapPort" - :disabled="loading" - required/> - <label for="man-imap-user"> - {{ t('mail', 'IMAP User') }} - </label> - <input type="text" - id="man-imap-user" - :placeholder="t('mail', 'IMAP User')" - v-model="manualConfig.imapUser" - :disabled="loading" - required/> - <label for="man-imap-password"> - {{ t('mail', 'IMAP Password') }} - </label> - <input type="password" - id="man-imap-password" - :placeholder="t('mail', 'IMAP Password')" - v-model="manualConfig.imapPassword" - :disabled="loading" - required/> + <h3>{{ t('mail', 'IMAP Settings') }}</h3> + <label for="man-imap-host">{{ t('mail', 'IMAP Host') }}</label> + <input + type="text" + id="man-imap-host" + :placeholder="t('mail', 'IMAP Host')" + v-model="manualConfig.imapHost" + :disabled="loading" + required + > + <h4>{{ t('mail', 'IMAP Security') }}</h4> + <div class="flex-row"> + <input + type="radio" + id="man-imap-sec-none" + name="man-imap-sec" + v-model="manualConfig.imapSslMode" + :disabled="loading" + @change="onImapSslModeChange" + value="none" + > + <label + class="button" + for="man-imap-sec-none" + :class="{primary: manualConfig.imapSslMode === 'none' }" + >{{ t('mail', 'None') }}</label> + <input + type="radio" + id="man-imap-sec-ssl" + name="man-imap-sec" + v-model="manualConfig.imapSslMode" + :disabled="loading" + @change="onImapSslModeChange" + value="ssl" + > + <label + class="button" + for="man-imap-sec-ssl" + :class="{primary: manualConfig.imapSslMode === 'ssl' }" + >{{ t('mail', 'SSL/TLS') }}</label> + <input + type="radio" + id="man-imap-sec-tls" + name="man-imap-sec" + v-model="manualConfig.imapSslMode" + :disabled="loading" + @change="onImapSslModeChange" + value="tls" + > + <label + class="button" + for="man-imap-sec-tls" + :class="{primary: manualConfig.imapSslMode === 'tls' }" + >{{ t('mail', 'STARTTLS') }}</label> + </div> + <label for="man-imap-port">{{ t('mail', 'IMAP Port') }}</label> + <input + type="number" + id="man-imap-port" + :placeholder="t('mail', 'IMAP Port')" + v-model="manualConfig.imapPort" + :disabled="loading" + required + > + <label for="man-imap-user">{{ t('mail', 'IMAP User') }}</label> + <input + type="text" + id="man-imap-user" + :placeholder="t('mail', 'IMAP User')" + v-model="manualConfig.imapUser" + :disabled="loading" + required + > + <label for="man-imap-password">{{ t('mail', 'IMAP Password') }}</label> + <input + type="password" + id="man-imap-password" + :placeholder="t('mail', 'IMAP Password')" + v-model="manualConfig.imapPassword" + :disabled="loading" + required + > - <h3>{{ t('mail', 'SMTP Settings') }}</h3> - <input type="text" - ref="smtpHost" - name="smtp-host" - :placeholder="t('mail', 'SMTP Host')" - v-model="manualConfig.smtpHost" - :disabled="loading" - required/> - <h4>{{ t('mail', 'SMTP Security') }}</h4> - <div class="flex-row"> - <input type="radio" - id="man-smtp-sec-none" - name="man-smtp-sec" - v-model="manualConfig.smtpSslMode" - :disabled="loading" - @change="onSmtpSslModeChange" - value="none"> - <label class="button" - for="man-smtp-sec-none" - :class="{primary: manualConfig.smtpSslMode === 'none' }"> - {{ t('mail', 'None') }} - </label> - <input type="radio" - id="man-smtp-sec-ssl" - name="man-smtp-sec" - v-model="manualConfig.smtpSslMode" - :disabled="loading" - @change="onSmtpSslModeChange" - value="ssl"> - <label class="button" - for="man-smtp-sec-ssl" - :class="{primary: manualConfig.smtpSslMode === 'ssl' }"> - {{ t('mail', 'SSL/TLS') }} - </label> - <input type="radio" - id="man-smtp-sec-tls" - name="man-smtp-sec" - v-model="manualConfig.smtpSslMode" - :disabled="loading" - @change="onSmtpSslModeChange" - value="tls"> - <label class="button" - for="man-smtp-sec-tls" - :class="{primary: manualConfig.smtpSslMode === 'tls' }"> - {{ t('mail', 'STARTTLS') }} - </label> - </div> - <label for="man-smtp-port"> - {{ t('mail', 'SMTP Port') }} - </label> - <input type="number" - id="man-smtp-port" - :placeholder="t('mail', 'SMTP Port')" - v-model="manualConfig.smtpPort" - :disabled="loading" - required/> - <label for="man-smtp-user"> - {{ t('mail', 'SMTP User') }} - </label> - <input type="text" - id="man-smtp-user" - :placeholder="t('mail', 'SMTP User')" - v-model="manualConfig.smtpUser" - :disabled="loading" - required/> - <label for="man-smtp-password"> - {{ t('mail', 'SMTP Password') }} - </label> - <input type="password" - id="man-smtp-password" - :placeholder="t('mail', 'SMTP Password')" - v-model="manualConfig.smtpPassword" - :disabled="loading" - required/> - </tab> - </tabs> - <input type="submit" - class="primary" - v-on:click="onSubmit" - :disabled="loading" - :value="t('mail', 'Connect')"/> - </div> + <h3>{{ t('mail', 'SMTP Settings') }}</h3> + <input + type="text" + ref="smtpHost" + name="smtp-host" + :placeholder="t('mail', 'SMTP Host')" + v-model="manualConfig.smtpHost" + :disabled="loading" + required + > + <h4>{{ t('mail', 'SMTP Security') }}</h4> + <div class="flex-row"> + <input + type="radio" + id="man-smtp-sec-none" + name="man-smtp-sec" + v-model="manualConfig.smtpSslMode" + :disabled="loading" + @change="onSmtpSslModeChange" + value="none" + > + <label + class="button" + for="man-smtp-sec-none" + :class="{primary: manualConfig.smtpSslMode === 'none' }" + >{{ t('mail', 'None') }}</label> + <input + type="radio" + id="man-smtp-sec-ssl" + name="man-smtp-sec" + v-model="manualConfig.smtpSslMode" + :disabled="loading" + @change="onSmtpSslModeChange" + value="ssl" + > + <label + class="button" + for="man-smtp-sec-ssl" + :class="{primary: manualConfig.smtpSslMode === 'ssl' }" + >{{ t('mail', 'SSL/TLS') }}</label> + <input + type="radio" + id="man-smtp-sec-tls" + name="man-smtp-sec" + v-model="manualConfig.smtpSslMode" + :disabled="loading" + @change="onSmtpSslModeChange" + value="tls" + > + <label + class="button" + for="man-smtp-sec-tls" + :class="{primary: manualConfig.smtpSslMode === 'tls' }" + >{{ t('mail', 'STARTTLS') }}</label> + </div> + <label for="man-smtp-port">{{ t('mail', 'SMTP Port') }}</label> + <input + type="number" + id="man-smtp-port" + :placeholder="t('mail', 'SMTP Port')" + v-model="manualConfig.smtpPort" + :disabled="loading" + required + > + <label for="man-smtp-user">{{ t('mail', 'SMTP User') }}</label> + <input + type="text" + id="man-smtp-user" + :placeholder="t('mail', 'SMTP User')" + v-model="manualConfig.smtpUser" + :disabled="loading" + required + > + <label for="man-smtp-password">{{ t('mail', 'SMTP Password') }}</label> + <input + type="password" + id="man-smtp-password" + :placeholder="t('mail', 'SMTP Password')" + v-model="manualConfig.smtpPassword" + :disabled="loading" + required + > + </tab> + </tabs> + <input + type="submit" + class="primary" + v-on:click="onSubmit" + :disabled="loading" + :value="submitButtonText" + > + </div> </template> <script> - import {Tab, Tabs} from 'vue-tabs-component' +import _ from 'underscore' +import { Tab, Tabs } from 'vue-tabs-component' - export default { - name: 'AccountForm', - props: { - displayName: { - type: String, - default: '', - }, - email: { - type: String, - default: '', - }, - save: { - type: Function, - required: true, - } +export default { + name: 'AccountForm', + props: { + displayName: { + type: String, + default: '', }, - components: { - Tab, - Tabs, + email: { + type: String, + default: '', }, - data () { - return { - loading: false, - mode: 'auto', - autoConfig: { - accountName: this.displayName, - emailAddress: this.email, - password: '', - }, - manualConfig: { - accountName: '', - emailAddress: '', - accountName: '', - imapHost: '', - imapPort: 993, - imapSslMode: 'ssl', - imapUser: '', - imapPassword: '', - smtpHost: '', - smtpPort: 587, - smtpSslMode: 'tls', - smtpUser: '', - smtpPassword: '', - } - }; + save: { + type: Function, + required: true, }, - methods: { - onModeChanged (e) { - this.mode = e.tab.id - - if (this.mode === 'manual') { - if (this.manualConfig.accountName === '') { - this.manualConfig.accountName = this.autoConfig.accountName - } - if (this.manualConfig.emailAddress === '') { - this.manualConfig.emailAddress = this.autoConfig.emailAddress - } + account: { + type: Object, + required: false, + }, + }, + components: { + Tab, + Tabs, + }, + data() { + const fromAccountOr = (prop, def) => { + if (!_.isUndefined(this.account)) { + return this.account[prop] + } else { + return def + } + } - // IMAP - if (this.manualConfig.imapUser === '') { - this.manualConfig.imapUser = this.autoConfig.emailAddress - } - if (this.manualConfig.imapPassword === '') { - this.manualConfig.imapPassword = this.autoConfig.password - } + return { + loading: false, + mode: 'auto', + autoConfig: { + accountName: this.displayName, + emailAddress: this.email, + password: '', + }, + manualConfig: { + accountName: '', + emailAddress: '', + imapHost: fromAccountOr('imapHost', ''), + imapPort: fromAccountOr('imapPort', 993), + imapSslMode: fromAccountOr('imapSslMode', 'ssl'), + imapUser: fromAccountOr('imapUser', ''), + imapPassword: '', + smtpHost: fromAccountOr('smtpHost', ''), + smtpPort: fromAccountOr('smtpPort', 587), + smtpSslMode: fromAccountOr('smtpSslMode', 'tls'), + smtpUser: fromAccountOr('smtpUser', ''), + smtpPassword: '', + }, + submitButtonText: this.account + ? t('mail', 'Save') + : t('mail', 'Connect'), + } + }, + computed: { + settingsPage() { + return !_.isUndefined(this.account) + }, + }, + methods: { + onModeChanged(e) { + this.mode = e.tab.id - // SMTP - if (this.manualConfig.smtpUser === '') { - this.manualConfig.smtpUser = this.autoConfig.emailAddress - } - if (this.manualConfig.smtpPassword === '') { - this.manualConfig.smtpPassword = this.autoConfig.password - } + if (this.mode === 'manual') { + if (this.manualConfig.accountName === '') { + this.manualConfig.accountName = this.autoConfig.accountName } - }, - onImapSslModeChange: function () { - switch (this.manualConfig.imapSslMode) { - case 'none': - case 'tls': - this.manualConfig.imapPort = 143; - break; - case 'ssl': - this.manualConfig.imapPort = 993; - break; + if (this.manualConfig.emailAddress === '') { + this.manualConfig.emailAddress = this.autoConfig.emailAddress } - }, - onSmtpSslModeChange: function () { - switch (this.manualConfig.smtpSslMode) { - case 'none': - case 'tls': - this.manualConfig.smtpPort = 587; - break; - case 'ssl': - this.manualConfig.smtpPort = 465; - break; + + // IMAP + if (this.manualConfig.imapUser === '') { + this.manualConfig.imapUser = this.autoConfig.emailAddress } - }, - saveChanges () { - if (this.mode === 'auto') { - return this.save({ - autoDetect: true, - ...this.autoConfig, - }) - } else { - return this.save({ - autoDetect: false, - ...this.manualConfig, - }) + if (this.manualConfig.imapPassword === '') { + this.manualConfig.imapPassword = this.autoConfig.password } - }, - onSubmit: function () { - this.loading = true - this.saveChanges() - .catch(console.error.bind(this)) - .then(() => this.loading = false) + // SMTP + if (this.manualConfig.smtpUser === '') { + this.manualConfig.smtpUser = this.autoConfig.emailAddress + } + if (this.manualConfig.smtpPassword === '') { + this.manualConfig.smtpPassword = this.autoConfig.password + } } - } - }; + }, + onImapSslModeChange: function() { + switch (this.manualConfig.imapSslMode) { + case 'none': + case 'tls': + this.manualConfig.imapPort = 143 + break + case 'ssl': + this.manualConfig.imapPort = 993 + break + } + }, + onSmtpSslModeChange: function() { + switch (this.manualConfig.smtpSslMode) { + case 'none': + case 'tls': + this.manualConfig.smtpPort = 587 + break + case 'ssl': + this.manualConfig.smtpPort = 465 + break + } + }, + saveChanges() { + if (this.mode === 'auto') { + return this.save({ + autoDetect: true, + ...this.autoConfig, + }) + } else { + return this.save({ + autoDetect: false, + ...this.manualConfig, + }) + } + }, + onSubmit: function() { + this.loading = true + + this.saveChanges() + .catch(console.error.bind(this)) + .then(() => (this.loading = false)) + }, + }, +} </script> <style> - .tabs-component-tabs { - display: flex; - } +.tabs-component-tabs { + display: flex; +} - .tabs-component-tab { - flex-grow: 1; - color: var(--color-text-lighter); - font-weight: bold; - } +.tabs-component-tab { + flex-grow: 1; + color: var(--color-text-lighter); + font-weight: bold; +} - .tabs-component-tab.is-active { - border-bottom: 1px solid black; - } +.tabs-component-tab.is-active { + border-bottom: 1px solid black; +} - .tabs-component-panels { - padding-top: 20px; - } +.tabs-component-panels { + padding-top: 20px; +} - .tabs-component-panels label { - text-align: left; - width: 100%; - display: inline-block; - } +.tabs-component-panels label { + text-align: left; + width: 100%; + display: inline-block; +} - .tabs-component-panels input, - .tabs-component-panels select { - margin-bottom: 10px; - } +.tabs-component-panels input, +.tabs-component-panels select { + margin-bottom: 10px; +} </style> <style scoped> - h4 { - text-align: left; - } +h4 { + text-align: left; +} - .flex-row { - display: flex; - } +.flex-row { + display: flex; +} - label.button { - display: inline-block; - text-align: center; - flex-grow: 1; - } +label.button { + display: inline-block; + text-align: center; + flex-grow: 1; +} - input[type="radio"] { - display: none; - } +input[type='radio'] { + display: none; +} - input[type=radio][disabled] + label { - cursor: default; - opacity: 0.5; - } +input[type='radio'][disabled] + label { + cursor: default; + opacity: 0.5; +} </style> diff --git a/src/service/AccountService.js b/src/service/AccountService.js index d1cf00193..dcb424a13 100644 --- a/src/service/AccountService.js +++ b/src/service/AccountService.js @@ -1,10 +1,10 @@ -import {generateUrl} from 'nextcloud-server/dist/router' +import { generateUrl } from 'nextcloud-server/dist/router' import HttpClient from 'nextcloud-axios' export const fixAccountId = original => { return { id: original.accountId, - ...original + ...original, } } @@ -16,6 +16,16 @@ export const create = data => { .then(fixAccountId) } +export const update = data => { + const url = generateUrl(`/apps/mail/api/accounts/{id}`, { + id: data.accountId, + }) + + return HttpClient.put(url, data) + .then(resp => resp.data) + .then(fixAccountId) +} + export const fetchAll = () => { const url = generateUrl('/apps/mail/api/accounts') @@ -24,7 +34,7 @@ export const fetchAll = () => { export const fetch = id => { const url = generateUrl('/apps/mail/api/accounts/{id}', { - id + id, }) return HttpClient.get(url).then(resp => fixAccountId(resp.data)) diff --git a/src/store/actions.js b/src/store/actions.js index 52ae67188..856575512 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -24,6 +24,7 @@ import _ from "lodash"; import {savePreference} from "../service/PreferenceService"; import { create as createAccount, + update as updateAccount, deleteAccount, fetch as fetchAccount, fetchAll as fetchAllAccounts @@ -69,6 +70,14 @@ export default { return account }) }, + updateAccount ({commit}, config) { + return updateAccount(config) + .then(account => { + console.debug('account updated', account) + commit('editAccount', account) + return account + }) + }, deleteAccount ({commit}, account) { return deleteAccount(account.id) .then(account => { diff --git a/src/store/actions.js.orig b/src/store/actions.js.orig new file mode 100644 index 000000000..c3ffb03f3 --- /dev/null +++ b/src/store/actions.js.orig @@ -0,0 +1,865 @@ +<<<<<<< HEAD:src/store.js +import _ from 'lodash' +import Vue from 'vue' +import Vuex from 'vuex' +import { translate as t } from 'nextcloud-server/dist/l10n' + +import { + create as createAccount, + update as updateAccount, + fetch as fetchAccount, + fetchAll as fetchAllAccounts, +} from './service/AccountService' +import { fetchAll as fetchAllFolders } from './service/FolderService' +======= +/* + * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import _ from "lodash"; + +import {savePreference} from "../service/PreferenceService"; +import { + create as createAccount, + deleteAccount, + fetch as fetchAccount, + fetchAll as fetchAllAccounts +} from "../service/AccountService"; +import {fetchAll as fetchAllFolders} from "../service/FolderService"; +>>>>>>> 12b4a7d848da1bf5d72db31f1a3b7e42c09af590:src/store/actions.js +import { + deleteMessage, + fetchEnvelopes, + fetchMessage, + setEnvelopeFlag, +<<<<<<< HEAD:src/store.js + syncEnvelopes, +} from './service/MessageService' +import { showNewMessagesNotification } from './service/NotificationService' +import { savePreference } from './service/PreferenceService' +import { parseUid } from './util/EnvelopeUidParser' +import { value } from './util/undefined' + +Vue.use(Vuex) + +const UNIFIED_ACCOUNT_ID = 0 +const UNIFIED_INBOX_ID = 'inbox' +const UNIFIED_INBOX_UID = UNIFIED_ACCOUNT_ID + '-' + UNIFIED_INBOX_ID + +export const mutations = { + savePreference(state, { key, value }) { + Vue.set(state.preferences, key, value) + }, + addAccount(state, account) { + account.folders = [] + account.collapsed = true + Vue.set(state.accounts, account.id, account) + Vue.set( + state, + 'accountList', + _.sortBy(state.accountList.concat([account.id])) + ) + }, + editAccount(state, account) { + Vue.set( + state.accounts, + account.id, + Object.assign({}, state.accounts[account.id], account) + ) + }, + toggleAccountCollapsed(state, accountId) { + state.accounts[accountId].collapsed = !state.accounts[accountId] + .collapsed + }, + addFolder(state, { account, folder }) { + const addToState = folder => { + const id = account.id + '-' + folder.id + folder.accountId = account.id + folder.envelopes = [] + folder.searchEnvelopes = [] + Vue.set(state.folders, id, folder) + return id + } + + // Add all folders (including subfolders ot state, but only toplevel to account + const id = addToState(folder) + folder.folders.forEach(addToState) + + account.folders.push(id) + }, + updateFolderSyncToken(state, { folder, syncToken }) { + folder.syncToken = syncToken + }, + addEnvelope(state, { accountId, folder, envelope }) { + const uid = accountId + '-' + folder.id + '-' + envelope.id + envelope.accountId = accountId + envelope.folderId = folder.id + envelope.uid = uid + Vue.set(state.envelopes, uid, envelope) + Vue.set( + folder, + 'envelopes', + _.sortedUniq( + _.orderBy( + folder.envelopes.concat([uid]), + id => state.envelopes[id].dateInt, + 'desc' + ) + ) + ) + }, + addSearchEnvelopes(state, { accountId, folder, envelopes, clear }) { + const uids = envelopes.map(envelope => { + const uid = accountId + '-' + folder.id + '-' + envelope.id + envelope.accountId = accountId + envelope.folderId = folder.id + envelope.uid = uid + Vue.set(state.envelopes, uid, envelope) + return uid + }) + + if (clear) { + Vue.set(folder, 'searchEnvelopes', uids) + } else { + Vue.set( + folder, + 'searchEnvelopes', + _.sortedUniq( + _.orderBy( + folder.searchEnvelopes.concat(uids), + id => state.envelopes[id].dateInt, + 'desc' + ) + ) + ) + } + }, + addUnifiedEnvelope(state, { folder, envelope }) { + Vue.set( + folder, + 'envelopes', + _.sortedUniq( + _.orderBy( + folder.envelopes.concat([envelope.uid]), + id => state.envelopes[id].dateInt, + 'desc' + ) + ) + ) + }, + addUnifiedEnvelopes(state, { folder, uids }) { + Vue.set(folder, 'envelopes', uids) + }, + addUnifiedSearchEnvelopes(state, { folder, uids }) { + Vue.set(folder, 'searchEnvelopes', uids) + }, + flagEnvelope(state, { envelope, flag, value }) { + envelope.flags[flag] = value + }, + removeEnvelope(state, { accountId, folder, id }) { + const envelopeUid = accountId + '-' + folder.id + '-' + id + const idx = folder.envelopes.indexOf(envelopeUid) + if (idx < 0) { + console.warn('envelope does not exist', accountId, folder.id, id) + return + } + folder.envelopes.splice(idx, 1) + + const unifiedAccount = state.accounts[UNIFIED_ACCOUNT_ID] + unifiedAccount.folders + .map(fId => state.folders[fId]) + .filter(f => f.specialRole === folder.specialRole) + .forEach(folder => { + const idx = folder.envelopes.indexOf(envelopeUid) + if (idx < 0) { + console.warn( + 'envelope does not exist in unified mailbox', + accountId, + folder.id, + id + ) + return + } + folder.envelopes.splice(idx, 1) + }) + + Vue.delete(folder.envelopes, envelopeUid) + }, + addMessage(state, { accountId, folderId, message }) { + const uid = accountId + '-' + folderId + '-' + message.id + message.accountId = accountId + message.folderId = folderId + message.uid = uid + Vue.set(state.messages, uid, message) + }, + updateDraft(state, { draft, data, newUid }) { + // Update draft's UID + const oldUid = draft.uid + const uid = draft.accountId + '-' + draft.folderId + '-' + newUid + console.debug('saving draft as UID ' + uid) + draft.uid = uid + + // TODO: strategy to keep the full draft object in sync, not just the visible + // changes + draft.subject = data.subject + + // Update ref in folder's envelope list + const envs = + state.folders[draft.accountId + '-' + draft.folderId].envelopes + const idx = envs.indexOf(oldUid) + if (idx < 0) { + console.warn( + 'not replacing draft ' + + oldUid + + ' in envelope list because it did not exist' + ) + } else { + envs[idx] = uid + } + + // Move message/envelope objects to new keys + Vue.delete(state.envelopes, oldUid) + Vue.delete(state.messages, oldUid) + Vue.set(state.envelopes, uid, draft) + Vue.set(state.messages, uid, draft) + }, + setMessageBodyText(state, { uid, bodyText }) { + Vue.set(state.messages[uid], 'bodyText', bodyText) + }, + removeMessage(state, { accountId, folderId, id }) { + Vue.delete(state.messages, accountId + '-' + folderId + '-' + id) + }, +} + +export const actions = { + savePreference({ commit, getters }, { key, value }) { + return savePreference(key, value).then(({ value }) => { + commit('savePreference', { + key, + value, +======= + syncEnvelopes +} from "../service/MessageService"; +import {showNewMessagesNotification} from "../service/NotificationService"; +import {parseUid} from "../util/EnvelopeUidParser"; + +export default { + savePreference ({commit, getters}, {key, value}) { + return savePreference(key, value) + .then(({value}) => { + commit('savePreference', { + key, + value, + }) +>>>>>>> 12b4a7d848da1bf5d72db31f1a3b7e42c09af590:src/store/actions.js + }) + }) + }, + fetchAccounts({ commit, getters }) { + return fetchAllAccounts().then(accounts => { + accounts.forEach(account => commit('addAccount', account)) + return getters.getAccounts() + }) + }, + fetchAccount({ commit }, id) { + return fetchAccount(id).then(account => { + commit('addAccount', account) + return account + }) + }, + createAccount({ commit }, config) { + return createAccount(config).then(account => { + commit('addAccount', account) + return account + }) + }, +<<<<<<< HEAD:src/store.js + updateAccount({ commit }, config) { + return updateAccount(config).then(account => { + console.debug('account updated', account) + commit('editAccount', account) + return account + }) + }, + fetchFolders({ commit, getters }, { accountId }) { +======= + deleteAccount ({commit}, account) { + return deleteAccount(account.id) + .then(account => { + console.debug('account deleted') + location.reload() // TODO: better handling of this + }) + .catch(err => { + console.error('could not delete account', err) + throw err + }) + }, + fetchFolders ({commit, getters}, {accountId}) { +>>>>>>> 12b4a7d848da1bf5d72db31f1a3b7e42c09af590:src/store/actions.js + if (getters.getAccount(accountId).isUnified) { + return Promise.resolve(getters.getFolders(accountId)) + } + + return fetchAllFolders(accountId).then(folders => { + let account = getters.getAccount(accountId) + + folders.forEach(folder => { + commit('addFolder', { + account, + folder, + }) + }) + return folders + }) + }, + fetchEnvelopes( + { state, commit, getters, dispatch }, + { accountId, folderId, query } + ) { + const folder = getters.getFolder(accountId, folderId) + const isSearch = !_.isUndefined(query) + if (folder.isUnified) { + // Fetch and combine envelopes of all individual folders + // + // The last envelope is excluded to efficiently build the next unified + // pages (fetch only individual pages that do not have sufficient local + // data) + // + // TODO: handle short/ending streams and show their last element as well + return Promise.all( + getters + .getAccounts() + .filter(account => !account.isUnified) + .map(account => + Promise.all( + getters + .getFolders(account.id) + .filter( + f => f.specialRole === folder.specialRole + ) + .map(folder => + dispatch('fetchEnvelopes', { + accountId: account.id, + folderId: folder.id, + query, + }) + ) + ) + ) + ) + .then(res => res.map(envs => envs.slice(0, 19))) + .then(res => _.flattenDepth(res, 2)) + .then(envs => _.orderBy(envs, env => env.dateInt, 'desc')) + .then(envs => _.slice(envs, 0, 19)) // 19 to handle non-overlapping streams + .then(envelopes => { + if (!isSearch) { + commit('addUnifiedEnvelopes', { + folder, + uids: envelopes.map(e => e.uid), + }) + } else { + commit('addUnifiedSearchEnvelopes', { + folder, + uids: envelopes.map(e => e.uid), + }) + } + return envelopes + }) + } + + return fetchEnvelopes(accountId, folderId, query).then(envelopes => { + let folder = getters.getFolder(accountId, folderId) + + if (!isSearch) { + envelopes.forEach(envelope => + commit('addEnvelope', { + accountId, + folder, + envelope, + }) + ) + } else { + commit('addSearchEnvelopes', { + accountId, + folder, + envelopes, + clear: true, + }) + } + return envelopes + }) + }, + fetchNextUnifiedEnvelopePage( + { state, commit, getters, dispatch }, + { accountId, folderId, query } + ) { + const folder = getters.getFolder(accountId, folderId) + const isSearch = !_.isUndefined(query) + const list = isSearch ? 'searchEnvelopes' : 'envelopes' + + // We only care about folders of the same type/role + const individualFolders = _.flatten( + getters + .getAccounts() + .filter(a => !a.isUnified) + .map(a => + getters + .getFolders(a.id) + .filter(f => f.specialRole === folder.specialRole) + ) + ) + // Build a sorted list of all currently known envelopes (except last elem) + const knownEnvelopes = _.orderBy( + _.flatten( + individualFolders.map(f => f[list].slice(0, f[list].length - 1)) + ), + id => state.envelopes[id].dateInt, + 'desc' + ) + // The index of the last element in the current unified mailbox determines + // the new offset + const tailId = _.last(folder[list]) + const tailIdx = knownEnvelopes.indexOf(tailId) + if (tailIdx === -1) { + return Promise.reject( + new Error( + 'current envelopes do not contain unified mailbox tail. this should not have happened' + ) + ) + } + + // Select the next page, based on offline data + const nextCandidates = knownEnvelopes.slice(tailIdx + 1, tailIdx + 20) + + // Now, let's check if any of the "streams" have reached their ends. + // In that case, we attempt to fetch more elements recursively + // + // In case of an empty next page we always fetch all streams (this might be redundant) + // + // Their end was reached if the last known (oldest) envelope is an element + // of the offline page + // TODO: what about streams that ended before? Is it safe to ignore those? + const needFetch = individualFolders + .filter(f => !_.isEmpty(f[list])) + .filter(f => { + const lastShown = f[list][f[list].length - 2] + return ( + nextCandidates.length <= 18 || + nextCandidates.indexOf(lastShown) !== -1 + ) + }) + + if (_.isEmpty(needFetch)) { + if (!isSearch) { + commit('addUnifiedEnvelopes', { + folder, + uids: _.sortedUniq( + _.orderBy( + folder[list].concat(nextCandidates), + id => state.envelopes[id].dateInt, + 'desc' + ) + ), + }) + } else { + commit('addUnifiedSearchEnvelopes', { + folder, + uids: _.sortedUniq( + _.orderBy( + folder[list].concat(nextCandidates), + id => state.envelopes[id].dateInt, + 'desc' + ) + ), + }) + } + } else { + return Promise.all( + needFetch.map(f => + dispatch('fetchNextEnvelopePage', { + accountId: f.accountId, + folderId: f.id, + query, + }) + ) + ).then(() => { + return dispatch('fetchNextUnifiedEnvelopePage', { + accountId, + folderId, + query, + }) + }) + } + }, + fetchNextEnvelopePage( + { commit, getters, dispatch }, + { accountId, folderId, query } + ) { + const folder = getters.getFolder(accountId, folderId) + const isSearch = !_.isUndefined(query) + const list = isSearch ? 'searchEnvelopes' : 'envelopes' + + if (folder.isUnified) { + return dispatch('fetchNextUnifiedEnvelopePage', { + accountId, + folderId, + query, + }) + } + + const lastEnvelopeId = folder[list][folder.envelopes.length - 1] + if (typeof lastEnvelopeId === 'undefined') { + console.error('folder is empty', folder[list]) + return Promise.reject( + new Error( + 'Local folder has no envelopes, cannot determine cursor' + ) + ) + } + const lastEnvelope = getters.getEnvelopeById(lastEnvelopeId) + if (typeof lastEnvelope === 'undefined') { + return Promise.reject( + new Error( + 'Cannot find last envelope. Required for the folder cursor' + ) + ) + } + + return fetchEnvelopes( + accountId, + folderId, + query, + lastEnvelope.dateInt + ).then(envelopes => { + if (!isSearch) { + envelopes.forEach(envelope => + commit('addEnvelope', { + accountId, + folder, + envelope, + }) + ) + } else { + commit('addSearchEnvelopes', { + accountId, + folder, + envelopes, + clear: false, + }) + } + + return envelopes + }) + }, + syncEnvelopes({ commit, getters, dispatch }, { accountId, folderId }) { + const folder = getters.getFolder(accountId, folderId) + + if (folder.isUnified) { + return Promise.all( + getters + .getAccounts() + .filter(account => !account.isUnified) + .map(account => + Promise.all( + getters + .getFolders(account.id) + .filter( + f => f.specialRole === folder.specialRole + ) + .map(folder => + dispatch('syncEnvelopes', { + accountId: account.id, + folderId: folder.id, + }) + ) + ) + ) + ) + } + + const syncToken = folder.syncToken + const uids = getters + .getEnvelopes(accountId, folderId) + .map(env => env.id) + + return syncEnvelopes(accountId, folderId, syncToken, uids).then( + syncData => { + const unifiedFolder = getters.getUnifiedFolder( + folder.specialRole + ) + + syncData.newMessages.forEach(envelope => { + commit('addEnvelope', { + accountId, + folder, + envelope, + }) + if (unifiedFolder) { + commit('addUnifiedEnvelope', { + folder: unifiedFolder, + envelope, + }) + } + }) + syncData.changedMessages.forEach(envelope => { + commit('addEnvelope', { + accountId, + folder, + envelope, + }) + }) + syncData.vanishedMessages.forEach(id => { + commit('removeEnvelope', { + accountId, + folder, + id, + }) + // Already removed from unified inbox + }) + commit('updateFolderSyncToken', { + folder, + syncToken: syncData.token, + }) + + return syncData.newMessages + } + ) + }, + syncInboxes({ getters, dispatch }) { + return Promise.all( + getters + .getAccounts() + .filter(a => !a.isUnified) + .map(account => { + return Promise.all( + getters.getFolders(account.id).map(folder => { + if (folder.specialRole !== 'inbox') { + return + } + + return dispatch('syncEnvelopes', { + accountId: account.id, + folderId: folder.id, + }) + }) + ) + }) + ).then(results => { + const newMessages = _.flatMapDeep(results).filter( + _.negate(_.isUndefined) + ) + if (newMessages.length > 0) { + showNewMessagesNotification(newMessages) + } + }) + }, + toggleEnvelopeFlagged({ commit, getters }, envelope) { + // Change immediately and switch back on error + const oldState = envelope.flags.flagged + commit('flagEnvelope', { + envelope, + flag: 'flagged', + value: !oldState, + }) + + setEnvelopeFlag( + envelope.accountId, + envelope.folderId, + envelope.id, + 'flagged', + !oldState + ).catch(e => { + console.error('could not toggle message flagged state', e) + + // Revert change + commit('flagEnvelope', { + envelope, + flag: 'flagged', + value: oldState, + }) + }) + }, + toggleEnvelopeSeen({ commit, getters }, envelope) { + // Change immediately and switch back on error + const oldState = envelope.flags.unseen + commit('flagEnvelope', { + envelope, + flag: 'unseen', + value: !oldState, + }) + + setEnvelopeFlag( + envelope.accountId, + envelope.folderId, + envelope.id, + 'unseen', + !oldState + ).catch(e => { + console.error('could not toggle message unseen state', e) + + // Revert change + commit('flagEnvelope', { + envelope, + flag: 'unseen', + value: oldState, + }) + }) + }, + fetchMessage({ commit }, uid) { + const { accountId, folderId, id } = parseUid(uid) + return fetchMessage(accountId, folderId, id).then(message => { + // Only commit if not undefined (not found) + if (message) { + commit('addMessage', { + accountId, + folderId, + message, + }) + } + + return message + }) + }, + replaceDraft({ getters, commit }, { draft, uid, data }) { + commit('updateDraft', { + draft, + data, + newUid: uid, + }) + }, + deleteMessage({ getters, commit }, envelope) { + const folder = getters.getFolder(envelope.accountId, envelope.folderId) + commit('removeEnvelope', { + accountId: envelope.accountId, + folder, + id: envelope.id, + }) + + return deleteMessage(envelope.accountId, envelope.folderId, envelope.id) + .then(() => { + commit('removeMessage', { + accountId: envelope.accountId, + folder, + id: envelope.id, + }) + console.log('message removed') + }) + .catch(err => { + console.error('could not delete message', err) + commit('addEnvelope', { + accountId: envelope.accountId, + folder, + envelope, + }) + throw err + }) +<<<<<<< HEAD:src/store.js + }, +} + +export const getters = { + getPreference: state => (key, def) => { + return value(state.preferences[key]).or(def) + }, + getAccount: state => id => { + return state.accounts[id] + }, + getAccounts: state => () => { + return state.accountList.map(id => state.accounts[id]) + }, + getFolder: state => (accountId, folderId) => { + return state.folders[accountId + '-' + folderId] + }, + getFolders: state => accountId => { + return state.accounts[accountId].folders.map( + folderId => state.folders[folderId] + ) + }, + getUnifiedFolder: state => specialRole => { + return _.head( + state.accounts[UNIFIED_ACCOUNT_ID].folders + .map(folderId => state.folders[folderId]) + .filter(folder => folder.specialRole === specialRole) + ) + }, + getEnvelope: state => (accountId, folderId, id) => { + return state.envelopes[accountId + '-' + folderId + '-' + id] + }, + getEnvelopeById: state => id => { + return state.envelopes[id] + }, + getEnvelopes: (state, getters) => (accountId, folderId) => { + return getters + .getFolder(accountId, folderId) + .envelopes.map(msgId => state.envelopes[msgId]) + }, + getSearchEnvelopes: (state, getters) => (accountId, folderId) => { + return getters + .getFolder(accountId, folderId) + .searchEnvelopes.map(msgId => state.envelopes[msgId]) + }, + getMessage: state => (accountId, folderId, id) => { + return state.messages[accountId + '-' + folderId + '-' + id] + }, + getMessageByUid: state => uid => { + return state.messages[uid] + }, +} + +export default new Vuex.Store({ + strict: process.env.NODE_ENV !== 'production', + state: { + preferences: {}, + accounts: { + [UNIFIED_ACCOUNT_ID]: { + id: UNIFIED_ACCOUNT_ID, + isUnified: true, + folders: [UNIFIED_INBOX_UID], + collapsed: false, + emailAddress: '', + name: '', + }, + }, + accountList: [UNIFIED_ACCOUNT_ID], + folders: { + [UNIFIED_INBOX_UID]: { + id: UNIFIED_INBOX_ID, + accountId: 0, + isUnified: true, + specialRole: 'inbox', + name: t('mail', 'All inboxes'), // TODO, + unread: 0, + folders: [], + envelopes: [], + searchEnvelopes: [], + }, + }, + envelopes: {}, + messages: {}, + autocompleteEntries: [], + }, + getters, + mutations, + actions, +}) +======= + } +} +>>>>>>> 12b4a7d848da1bf5d72db31f1a3b7e42c09af590:src/store/actions.js diff --git a/src/store/mutations.js b/src/store/mutations.js index 1c8c4898a..4da2571cd 100644 --- a/src/store/mutations.js +++ b/src/store/mutations.js @@ -38,6 +38,13 @@ export default { _.sortBy(state.accountList.concat([account.id])) ) }, + editAccount(state, account) { + Vue.set( + state.accounts, + account.id, + Object.assign({}, state.accounts[account.id], account) + ) + }, toggleAccountCollapsed (state, accountId) { state.accounts[accountId].collapsed = !state.accounts[accountId].collapsed }, diff --git a/src/views/AccountSettings.vue b/src/views/AccountSettings.vue index 4d0406345..9f5351e28 100644 --- a/src/views/AccountSettings.vue +++ b/src/views/AccountSettings.vue @@ -1,8 +1,18 @@ <template> <AppContent app-name="mail"> - <Navigation slot="navigation" /> + <Navigation slot="navigation"/> <template slot="content"> - TODO: implement account settings + <div class="section" id="account-info"> + <h2>{{ t ('mail', 'Account Settings') }} - {{ email }}</h2> + </div> + <div class="section"> + <div id="mail-settings"> + <AccountForm :displayName="displayName" + :email="email" + :save="onSave" + :account="account"/> + </div> + </div> </template> </AppContent> </template> @@ -10,20 +20,48 @@ <script> import {AppContent} from 'nextcloud-vue' - import AppSettingsMenu from '../components/AppSettingsMenu' + import AccountForm from '../components/AccountForm' import Navigation from '../components/Navigation' export default { name: 'AccountSettings', components: { + AccountForm, AppContent, Navigation, - AppSettingsMenu, }, + extends: SidebarItems, computed: { menu () { return this.buildMenu() - } - } + }, + account () { + return this.$store.getters.getAccount(this.$route.params.accountId) + }, + displayName () { + return this.$store.getters.getAccount(this.$route.params.accountId) + .name + }, + email () { + return this.$store.getters.getAccount(this.$route.params.accountId) + .emailAddress + }, + }, + methods: { + onSave (data) { + console.log('data to save:', data) + return this.$store + .dispatch('updateAccount', { + ...data, + accountId: this.$route.params.accountId, + }) + .then(account => account) + .catch(err => { + console.error('account update failed:', err) + + throw err + }) + }, + }, } </script> |